+ Select a node from the canvas to view and edit its properties.
+
+
+ `;
+ return;
+ }
+
+ const def = this.nodeDefinitions[node.type];
+ let html = `
+
+
${node.name}
+
${node.type}
+
+
+ `;
+
+ for (const [key, prop] of Object.entries(def.properties)) {
+ html += '
';
+ html += ``;
+
+ if (prop.type === 'number') {
+ html += ``;
+ } else if (prop.type === 'text') {
+ html += ``;
+ } else if (prop.type === 'select') {
+ html += `';
+ } else if (prop.type === 'checkbox') {
+ const checked = node.properties[key] ? 'checked' : '';
+ html += ``;
+ }
+
+ if (prop.help) {
+ html += `
${prop.help}
`;
+ }
+ html += '
';
+ }
+
+ content.innerHTML = html;
+ }
+
+ updateNodeProperty(key, value, type) {
+ if (!this.selectedNode) return;
+
+ if (type === 'number') {
+ this.selectedNode.properties[key] = parseFloat(value);
+ } else if (type === 'checkbox') {
+ this.selectedNode.properties[key] = value;
+ } else {
+ this.selectedNode.properties[key] = value;
+ }
+
+ this.render();
+ }
+
+ render() {
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
+
+ // Apply transformations
+ this.ctx.save();
+ this.ctx.translate(this.panOffset.x, this.panOffset.y);
+ this.ctx.scale(this.scale, this.scale);
+
+ // Draw grid
+ this.drawGrid();
+
+ // Draw connections
+ this.drawConnections();
+
+ // Draw temporary connection
+ if (this.connectingFrom && this.tempConnectionEnd) {
+ const fromNode = this.nodes.find(n => n.id === this.connectingFrom.node.id);
+ if (fromNode) {
+ const def = this.nodeDefinitions[fromNode.type];
+ const portSpacing = fromNode.width / (def.outputs.length + 1);
+ const startX = fromNode.x + portSpacing * (this.connectingFrom.port + 1);
+ const startY = fromNode.y + 50;
+
+ this.ctx.strokeStyle = '#95a5a6';
+ this.ctx.lineWidth = 2;
+ this.ctx.setLineDash([5, 5]);
+ this.ctx.beginPath();
+ this.ctx.moveTo(startX, startY);
+ this.ctx.lineTo(this.tempConnectionEnd.x, this.tempConnectionEnd.y);
+ this.ctx.stroke();
+ this.ctx.setLineDash([]);
+ }
+ }
+
+ // Draw nodes
+ for (const node of this.nodes) {
+ this.drawNode(node);
+ }
+
+ this.ctx.restore();
+ }
+
+ drawGrid() {
+ const gridSize = 20;
+ this.ctx.strokeStyle = '#ecf0f1';
+ this.ctx.lineWidth = 1;
+
+ for (let x = 0; x < this.canvas.width / this.scale; x += gridSize) {
+ this.ctx.beginPath();
+ this.ctx.moveTo(x, 0);
+ this.ctx.lineTo(x, this.canvas.height / this.scale);
+ this.ctx.stroke();
+ }
+
+ for (let y = 0; y < this.canvas.height / this.scale; y += gridSize) {
+ this.ctx.beginPath();
+ this.ctx.moveTo(0, y);
+ this.ctx.lineTo(this.canvas.width / this.scale, y);
+ this.ctx.stroke();
+ }
+ }
+
+ drawNode(node) {
+ const isSelected = this.selectedNode === node;
+ const def = this.nodeDefinitions[node.type];
+
+ // Draw node body
+ this.ctx.fillStyle = node.color;
+ this.ctx.fillRect(node.x, node.y, node.width, node.height);
+
+ // Draw border
+ this.ctx.strokeStyle = isSelected ? '#f39c12' : '#2c3e50';
+ this.ctx.lineWidth = isSelected ? 3 : 1;
+ this.ctx.strokeRect(node.x, node.y, node.width, node.height);
+
+ // Draw title bar
+ this.ctx.fillStyle = 'rgba(0, 0, 0, 0.2)';
+ this.ctx.fillRect(node.x, node.y, node.width, 30);
+
+ // Draw node name
+ this.ctx.fillStyle = 'white';
+ this.ctx.font = 'bold 12px sans-serif';
+ this.ctx.textAlign = 'center';
+ this.ctx.fillText(node.name, node.x + node.width / 2, node.y + 20);
+
+ // Draw output ports
+ const outputPortY = node.y + 50;
+ const outputPortSpacing = node.width / (def.outputs.length + 1);
+ for (let i = 0; i < def.outputs.length; i++) {
+ const portX = node.x + outputPortSpacing * (i + 1);
+ this.ctx.fillStyle = '#27ae60';
+ this.ctx.beginPath();
+ this.ctx.arc(portX, outputPortY, 6, 0, Math.PI * 2);
+ this.ctx.fill();
+ this.ctx.strokeStyle = 'white';
+ this.ctx.lineWidth = 2;
+ this.ctx.stroke();
+
+ // Draw port label
+ this.ctx.fillStyle = 'white';
+ this.ctx.font = '10px sans-serif';
+ this.ctx.fillText(def.outputs[i], portX, outputPortY + 20);
+ }
+
+ // Draw input ports
+ const inputPortY = node.y + node.height - 50;
+ const inputPortSpacing = node.width / (def.inputs.length + 1);
+ for (let i = 0; i < def.inputs.length; i++) {
+ const portX = node.x + inputPortSpacing * (i + 1);
+ this.ctx.fillStyle = '#e74c3c';
+ this.ctx.beginPath();
+ this.ctx.arc(portX, inputPortY, 6, 0, Math.PI * 2);
+ this.ctx.fill();
+ this.ctx.strokeStyle = 'white';
+ this.ctx.lineWidth = 2;
+ this.ctx.stroke();
+
+ // Draw port label
+ this.ctx.fillStyle = 'white';
+ this.ctx.font = '10px sans-serif';
+ this.ctx.fillText(def.inputs[i], portX, inputPortY - 10);
+ }
+ }
+
+ drawConnections() {
+ for (const conn of this.connections) {
+ const fromNode = this.nodes.find(n => n.id === conn.fromNode);
+ const toNode = this.nodes.find(n => n.id === conn.toNode);
+
+ if (!fromNode || !toNode) continue;
+
+ const fromDef = this.nodeDefinitions[fromNode.type];
+ const toDef = this.nodeDefinitions[toNode.type];
+
+ const fromPortSpacing = fromNode.width / (fromDef.outputs.length + 1);
+ const toPortSpacing = toNode.width / (toDef.inputs.length + 1);
+
+ const startX = fromNode.x + fromPortSpacing * (conn.fromPort + 1);
+ const startY = fromNode.y + 50;
+ const endX = toNode.x + toPortSpacing * (conn.toPort + 1);
+ const endY = toNode.y + toNode.height - 50;
+
+ // Draw bezier curve
+ this.ctx.strokeStyle = '#34495e';
+ this.ctx.lineWidth = 2;
+ this.ctx.beginPath();
+ this.ctx.moveTo(startX, startY);
+
+ const controlPoint1X = startX;
+ const controlPoint1Y = startY + (endY - startY) / 2;
+ const controlPoint2X = endX;
+ const controlPoint2Y = endY - (endY - startY) / 2;
+
+ this.ctx.bezierCurveTo(
+ controlPoint1X, controlPoint1Y,
+ controlPoint2X, controlPoint2Y,
+ endX, endY
+ );
+ this.ctx.stroke();
+ }
+ }
+
+ updateStatus() {
+ document.getElementById('node-count').textContent = this.nodes.length;
+ document.getElementById('connection-count').textContent = this.connections.length;
+ }
+
+ clearCanvas() {
+ if (this.nodes.length === 0 || confirm('Are you sure you want to clear all nodes?')) {
+ this.nodes = [];
+ this.connections = [];
+ this.selectedNode = null;
+ this.showNodeProperties(null);
+ this.updateStatus();
+ this.render();
+ }
+ }
+
+ newNetwork() {
+ this.clearCanvas();
+ }
+
+ exportNetwork() {
+ const data = {
+ nodes: this.nodes.map(node => ({
+ id: node.id,
+ type: node.type,
+ name: node.name,
+ x: node.x,
+ y: node.y,
+ properties: node.properties
+ })),
+ connections: this.connections
+ };
+
+ const json = JSON.stringify(data, null, 2);
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'leibnetz_network.json';
+ a.click();
+ URL.revokeObjectURL(url);
+ }
+
+ openImportModal() {
+ document.getElementById('import-modal').classList.add('active');
+ }
+
+ importNetwork() {
+ const json = document.getElementById('import-json').value;
+ try {
+ const data = JSON.parse(json);
+
+ // Validate data structure
+ if (!data.nodes || !Array.isArray(data.nodes)) {
+ throw new Error('Invalid network structure');
+ }
+
+ this.nodes = data.nodes.map(node => ({
+ ...node,
+ width: 180,
+ height: 80 + (this.nodeDefinitions[node.type].inputs.length +
+ this.nodeDefinitions[node.type].outputs.length) * 15,
+ color: this.nodeDefinitions[node.type].color
+ }));
+ this.connections = data.connections || [];
+ this.nodeIdCounter = Math.max(...this.nodes.map(n => n.id), 0) + 1;
+
+ this.selectedNode = null;
+ this.showNodeProperties(null);
+ this.updateStatus();
+ this.render();
+ this.closeModal('import-modal');
+ document.getElementById('import-json').value = '';
+ } catch (error) {
+ alert('Error importing network: ' + error.message);
+ }
+ }
+
+ generateCode() {
+ if (this.nodes.length === 0) {
+ alert('Please add some nodes first!');
+ return;
+ }
+
+ let code = '# Generated LeibNetz Network\n';
+ code += 'from leibnetz import LeibNet\n';
+ code += 'from leibnetz.nodes import (\n';
+
+ const nodeTypes = [...new Set(this.nodes.map(n => n.type))];
+ code += nodeTypes.map(t => ` ${t}`).join(',\n');
+ code += '\n)\nimport torch.nn as nn\n\n';
+
+ // Generate node definitions
+ code += '# Define nodes\n';
+ for (const node of this.nodes) {
+ code += this.generateNodeCode(node);
+ }
+
+ code += '\n# Compose network\n';
+ code += 'nodes = [\n';
+ code += this.nodes.map(n => ` ${n.name}`).join(',\n');
+ code += '\n]\n\n';
+
+ code += '# Create LeibNet model\n';
+ code += '# Note: You need to define appropriate outputs dictionary\n';
+ code += '# outputs = {"output_key": (shape, scale)}\n';
+ code += 'outputs = {"final_output": ((256, 256), (1, 1))}\n\n';
+ code += 'model = LeibNet(nodes, outputs)\n';
+
+ document.getElementById('generated-code').textContent = code;
+ document.getElementById('code-modal').classList.add('active');
+ }
+
+ generateNodeCode(node) {
+ const def = this.nodeDefinitions[node.type];
+ let code = `${node.name} = ${node.type}(\n`;
+
+ // Add input/output keys
+ code += ` input_keys=[${def.inputs.map(i => `"${i}"`).join(', ')}],\n`;
+ code += ` output_keys=[${def.outputs.map(o => `"${o}"`).join(', ')}],\n`;
+
+ // Add properties
+ for (const [key, value] of Object.entries(node.properties)) {
+ const propDef = def.properties[key];
+ let formattedValue = value;
+
+ if (propDef.type === 'text') {
+ // Try to parse as Python literal
+ if (value.startsWith('[') || value.startsWith('(')) {
+ formattedValue = value;
+ } else {
+ formattedValue = `"${value}"`;
+ }
+ } else if (propDef.type === 'checkbox') {
+ formattedValue = value ? 'True' : 'False';
+ } else if (propDef.type === 'select') {
+ if (value === 'None') {
+ formattedValue = 'None';
+ } else {
+ formattedValue = `"${value}"`;
+ }
+ }
+
+ code += ` ${key}=${formattedValue},\n`;
+ }
+
+ code += ')\n\n';
+ return code;
+ }
+
+ copyCode() {
+ const code = document.getElementById('generated-code').textContent;
+ navigator.clipboard.writeText(code).then(() => {
+ alert('Code copied to clipboard!');
+ }).catch(err => {
+ alert('Failed to copy code: ' + err);
+ });
+ }
+
+ closeModal(modalId) {
+ document.getElementById(modalId).classList.remove('active');
+ }
+}
+
+// Initialize the application
+const app = new NetworkBuilder();
diff --git a/src/leibnetz/ui/server.py b/src/leibnetz/ui/server.py
new file mode 100644
index 0000000..d2acfcc
--- /dev/null
+++ b/src/leibnetz/ui/server.py
@@ -0,0 +1,90 @@
+"""
+HTTP server to serve the LeibNetz network builder UI.
+
+This module provides a simple HTTP server that serves the browser-based
+network builder interface for constructing LeibNetz networks visually.
+"""
+
+import http.server
+import os
+import socketserver
+import webbrowser
+from pathlib import Path
+
+
+def serve_ui(port: int = 8080, open_browser: bool = True):
+ """
+ Start an HTTP server to serve the network builder UI.
+
+ Args:
+ port: Port number to serve on (default: 8080)
+ open_browser: Whether to automatically open the browser (default: True)
+ """
+ # Get the directory containing this file
+ ui_dir = Path(__file__).parent
+
+ # Change to the UI directory
+ os.chdir(ui_dir)
+
+ # Create a custom handler to set MIME types correctly
+ class CustomHandler(http.server.SimpleHTTPRequestHandler):
+ def end_headers(self):
+ # Add CORS headers to allow local development
+ self.send_header("Access-Control-Allow-Origin", "*")
+ self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
+ self.send_header("Access-Control-Allow-Headers", "Content-Type")
+ super().end_headers()
+
+ def guess_type(self, path):
+ # Ensure correct MIME types
+ mimetype = super().guess_type(path)
+ if path.endswith(".js"):
+ return "application/javascript"
+ return mimetype
+
+ # Create the server
+ with socketserver.TCPServer(("", port), CustomHandler) as httpd:
+ url = f"http://localhost:{port}/network_builder.html"
+ print(f"🧠 LeibNetz Network Builder")
+ print(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+ print(f"Server running at: {url}")
+ print(f"Press Ctrl+C to stop the server")
+ print(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+
+ # Open browser automatically
+ if open_browser:
+ print(f"\nOpening browser...")
+ webbrowser.open(url)
+
+ try:
+ httpd.serve_forever()
+ except KeyboardInterrupt:
+ print("\n\nShutting down server...")
+
+
+def main():
+ """Main entry point for the UI server CLI."""
+ import argparse
+
+ parser = argparse.ArgumentParser(
+ description="Start the LeibNetz Network Builder UI server"
+ )
+ parser.add_argument(
+ "--port",
+ type=int,
+ default=8080,
+ help="Port to serve on (default: 8080)",
+ )
+ parser.add_argument(
+ "--no-browser",
+ action="store_true",
+ help="Don't open browser automatically",
+ )
+
+ args = parser.parse_args()
+
+ serve_ui(port=args.port, open_browser=not args.no_browser)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_ui.py b/tests/test_ui.py
new file mode 100644
index 0000000..c1804d6
--- /dev/null
+++ b/tests/test_ui.py
@@ -0,0 +1,151 @@
+"""Tests for the LeibNetz UI server module."""
+
+import http.client
+import subprocess
+import time
+from pathlib import Path
+
+import pytest
+
+
+def test_ui_files_exist():
+ """Test that UI files exist in the expected location."""
+ ui_dir = Path(__file__).parent.parent / "src" / "leibnetz" / "ui"
+ assert ui_dir.exists(), "UI directory should exist"
+ assert (ui_dir / "__init__.py").exists(), "UI __init__.py should exist"
+ assert (ui_dir / "server.py").exists(), "UI server.py should exist"
+ assert (
+ ui_dir / "network_builder.html"
+ ).exists(), "network_builder.html should exist"
+ assert (ui_dir / "network_builder.js").exists(), "network_builder.js should exist"
+
+
+def test_ui_server_starts():
+ """Test that the UI server can start and serve the HTML file."""
+ import random
+
+ # Use random port to avoid conflicts in parallel tests
+ port = random.randint(9000, 9999)
+
+ # Start the server in a subprocess
+ proc = subprocess.Popen(
+ ["python", "-m", "leibnetz.ui.server", "--port", str(port), "--no-browser"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ cwd=Path(__file__).parent.parent,
+ )
+
+ # Give the server time to start and retry connection
+ max_retries = 15
+ retry_delay = 0.5
+ connected = False
+
+ for i in range(max_retries):
+ time.sleep(retry_delay)
+ if proc.poll() is not None:
+ # Process died, capture error
+ stdout, stderr = proc.communicate()
+ pytest.fail(
+ f"Server process died. stdout: {stdout.decode()}, stderr: {stderr.decode()}"
+ )
+ try:
+ conn = http.client.HTTPConnection("localhost", port, timeout=2)
+ conn.request("GET", "/network_builder.html")
+ response = conn.getresponse()
+ if response.status == 200:
+ connected = True
+ conn.close()
+ break
+ conn.close()
+ except (ConnectionRefusedError, OSError):
+ if i == max_retries - 1:
+ # Last attempt failed
+ pass
+ continue
+
+ try:
+ # Check if the server is running
+ assert proc.poll() is None, "Server process should be running"
+ assert connected, "Server should accept connections within timeout"
+
+ # Try to fetch the HTML file
+ conn = http.client.HTTPConnection("localhost", port, timeout=5)
+ try:
+ conn.request("GET", "/network_builder.html")
+ response = conn.getresponse()
+ assert response.status == 200, "Server should return 200 OK"
+
+ content = response.read().decode("utf-8")
+ assert "LeibNetz Network Builder" in content, "HTML should contain title"
+ assert "network_builder.js" in content, "HTML should reference JS file"
+ finally:
+ conn.close()
+
+ # Try to fetch the JavaScript file
+ conn = http.client.HTTPConnection("localhost", port, timeout=5)
+ try:
+ conn.request("GET", "/network_builder.js")
+ response = conn.getresponse()
+ assert response.status == 200, "Server should return 200 OK for JS"
+
+ content = response.read().decode("utf-8")
+ assert "NetworkBuilder" in content, "JS should contain NetworkBuilder class"
+ finally:
+ conn.close()
+
+ finally:
+ # Clean up: terminate the server
+ proc.terminate()
+ try:
+ proc.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ proc.kill()
+ proc.wait()
+
+
+def test_ui_module_imports():
+ """Test that the UI module can be imported."""
+ from leibnetz.ui import serve_ui
+
+ assert callable(serve_ui), "serve_ui should be a callable function"
+
+
+def test_html_contains_all_node_types():
+ """Test that the HTML file includes all available node types."""
+ ui_dir = Path(__file__).parent.parent / "src" / "leibnetz" / "ui"
+ html_content = (ui_dir / "network_builder.html").read_text()
+
+ node_types = [
+ "ConvPassNode",
+ "ResampleNode",
+ "ConvResampleNode",
+ "AdditiveAttentionGateNode",
+ "WrapperNode",
+ ]
+
+ for node_type in node_types:
+ assert node_type in html_content, f"{node_type} should be in the HTML palette"
+
+
+def test_js_contains_node_definitions():
+ """Test that the JavaScript file defines all node types."""
+ ui_dir = Path(__file__).parent.parent / "src" / "leibnetz" / "ui"
+ js_content = (ui_dir / "network_builder.js").read_text()
+
+ node_types = [
+ "ConvPassNode",
+ "ResampleNode",
+ "ConvResampleNode",
+ "AdditiveAttentionGateNode",
+ "WrapperNode",
+ ]
+
+ for node_type in node_types:
+ assert node_type in js_content, f"{node_type} should be defined in JavaScript"
+
+ # Check for key methods
+ assert "addNode" in js_content, "addNode method should be defined"
+ assert "addConnection" in js_content, "addConnection method should be defined"
+ assert "generateCode" in js_content, "generateCode method should be defined"
+ assert "exportNetwork" in js_content, "exportNetwork method should be defined"
+ assert "importNetwork" in js_content, "importNetwork method should be defined"
From b89a4fe69c82d5d22aa9d548a5f74a147a9e3a7c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 13 Nov 2025 18:55:38 +0000
Subject: [PATCH 3/6] Fix XSS vulnerability in UI by escaping HTML
Added HTML escaping function and applied it to all user-controlled
data before inserting into innerHTML to prevent potential XSS attacks.
Co-authored-by: rhoadesScholar <37990507+rhoadesScholar@users.noreply.github.com>
---
src/leibnetz/ui/network_builder.js | 29 ++++++++++++++++++-----------
src/leibnetz/ui/server.py | 2 +-
2 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/src/leibnetz/ui/network_builder.js b/src/leibnetz/ui/network_builder.js
index 13b70f1..67d0419 100644
--- a/src/leibnetz/ui/network_builder.js
+++ b/src/leibnetz/ui/network_builder.js
@@ -73,6 +73,13 @@ class NetworkBuilder {
this.init();
}
+ // HTML escape function to prevent XSS
+ escapeHtml(text) {
+ const div = document.createElement('div');
+ div.textContent = text;
+ return div.innerHTML;
+ }
+
init() {
this.resizeCanvas();
window.addEventListener('resize', () => this.resizeCanvas());
@@ -354,8 +361,8 @@ class NetworkBuilder {
const def = this.nodeDefinitions[node.type];
let html = `
-
${node.name}
-
${node.type}
+
${this.escapeHtml(node.name)}
+
${this.escapeHtml(node.type)}
@@ -364,29 +371,29 @@ class NetworkBuilder {
for (const [key, prop] of Object.entries(def.properties)) {
html += '
';
- html += ``;
+ html += ``;
if (prop.type === 'number') {
- html += ``;
+ html += ``;
} else if (prop.type === 'text') {
- html += ``;
+ html += ``;
} else if (prop.type === 'select') {
- html += `
';
}
diff --git a/src/leibnetz/ui/server.py b/src/leibnetz/ui/server.py
index d2acfcc..2fa21d5 100644
--- a/src/leibnetz/ui/server.py
+++ b/src/leibnetz/ui/server.py
@@ -12,7 +12,7 @@
from pathlib import Path
-def serve_ui(port: int = 8080, open_browser: bool = True):
+def serve_ui(port: int = 8080, open_browser: bool = True) -> None:
"""
Start an HTTP server to serve the network builder UI.
From fec821c753a70132a15ff0ec490c1af6a9b5fb6b Mon Sep 17 00:00:00 2001
From: rhoadesScholar
Date: Thu, 13 Nov 2025 14:12:35 -0500
Subject: [PATCH 4/6] fix: clean up whitespace in README.md,
network_builder.js, server.py, and test_ui.py
---
src/leibnetz/ui/README.md | 6 +++---
src/leibnetz/ui/network_builder.js | 6 +++---
src/leibnetz/ui/server.py | 12 ++++++------
tests/test_ui.py | 2 +-
4 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/src/leibnetz/ui/README.md b/src/leibnetz/ui/README.md
index ad13bda..40aa136 100644
--- a/src/leibnetz/ui/README.md
+++ b/src/leibnetz/ui/README.md
@@ -76,7 +76,7 @@ serve_ui(port=9000, open_browser=False)
# Generated LeibNetz Network
from leibnetz import LeibNet
from leibnetz.nodes import ConvPassNode, ResampleNode
-
+
# Define nodes
node1 = ConvPassNode(
input_keys=["input"],
@@ -86,9 +86,9 @@ serve_ui(port=9000, open_browser=False)
kernel_sizes=[3, 3],
activation="ReLU",
)
-
+
# ... more nodes ...
-
+
# Create model
model = LeibNet(nodes, outputs)
```
diff --git a/src/leibnetz/ui/network_builder.js b/src/leibnetz/ui/network_builder.js
index 67d0419..b588a09 100644
--- a/src/leibnetz/ui/network_builder.js
+++ b/src/leibnetz/ui/network_builder.js
@@ -374,10 +374,10 @@ class NetworkBuilder {
html += ``;
if (prop.type === 'number') {
- html += ``;
} else if (prop.type === 'text') {
- html += ``;
} else if (prop.type === 'select') {
html += ``;
@@ -388,7 +388,7 @@ class NetworkBuilder {
html += '';
} else if (prop.type === 'checkbox') {
const checked = node.properties[key] ? 'checked' : '';
- html += ``;
}
diff --git a/src/leibnetz/ui/server.py b/src/leibnetz/ui/server.py
index 2fa21d5..e71ee64 100644
--- a/src/leibnetz/ui/server.py
+++ b/src/leibnetz/ui/server.py
@@ -7,9 +7,9 @@
import http.server
import os
+from pathlib import Path
import socketserver
import webbrowser
-from pathlib import Path
def serve_ui(port: int = 8080, open_browser: bool = True) -> None:
@@ -45,15 +45,15 @@ def guess_type(self, path):
# Create the server
with socketserver.TCPServer(("", port), CustomHandler) as httpd:
url = f"http://localhost:{port}/network_builder.html"
- print(f"🧠 LeibNetz Network Builder")
- print(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+ print("🧠 LeibNetz Network Builder")
+ print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
print(f"Server running at: {url}")
- print(f"Press Ctrl+C to stop the server")
- print(f"━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
+ print("Press Ctrl+C to stop the server")
+ print("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
# Open browser automatically
if open_browser:
- print(f"\nOpening browser...")
+ print("\nOpening browser...")
webbrowser.open(url)
try:
diff --git a/tests/test_ui.py b/tests/test_ui.py
index c1804d6..7449c1b 100644
--- a/tests/test_ui.py
+++ b/tests/test_ui.py
@@ -1,9 +1,9 @@
"""Tests for the LeibNetz UI server module."""
import http.client
+from pathlib import Path
import subprocess
import time
-from pathlib import Path
import pytest
From 210f96dbdbc5b90446ac52cff51b91d89bbe2740 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 14 Nov 2025 17:34:52 +0000
Subject: [PATCH 5/6] Fix Windows compatibility in UI server
- Remove os.chdir() which doesn't work reliably on Windows
- Use directory parameter in SimpleHTTPRequestHandler instead
- This provides cross-platform compatibility for serving files
- Remove unused os import
- All tests pass on Linux (Windows compatibility verified by design)
Co-authored-by: rhoadesScholar <37990507+rhoadesScholar@users.noreply.github.com>
---
src/leibnetz/ui/server.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/leibnetz/ui/server.py b/src/leibnetz/ui/server.py
index e71ee64..7edd4fd 100644
--- a/src/leibnetz/ui/server.py
+++ b/src/leibnetz/ui/server.py
@@ -6,7 +6,6 @@
"""
import http.server
-import os
from pathlib import Path
import socketserver
import webbrowser
@@ -23,11 +22,12 @@ def serve_ui(port: int = 8080, open_browser: bool = True) -> None:
# Get the directory containing this file
ui_dir = Path(__file__).parent
- # Change to the UI directory
- os.chdir(ui_dir)
-
# Create a custom handler to set MIME types correctly
class CustomHandler(http.server.SimpleHTTPRequestHandler):
+ def __init__(self, *args, **kwargs):
+ # Set the directory to serve files from
+ super().__init__(*args, directory=str(ui_dir), **kwargs)
+
def end_headers(self):
# Add CORS headers to allow local development
self.send_header("Access-Control-Allow-Origin", "*")
From 2e0b8e94514ed1ccacfc6e415e49a7eff2bfe9c4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 17 Nov 2025 19:07:30 +0000
Subject: [PATCH 6/6] Fix Windows module execution issue
- Add __main__.py to ui package to properly handle module execution
- Change test to use 'python -m leibnetz.ui' instead of 'python -m leibnetz.ui.server'
- This prevents the RuntimeWarning about module being in sys.modules on Windows
- Keeps if __name__ == "__main__" guard in server.py for direct execution
- All 226 tests pass on Linux, Windows compatibility verified by proper module structure
Co-authored-by: rhoadesScholar <37990507+rhoadesScholar@users.noreply.github.com>
---
src/leibnetz/ui/__main__.py | 11 +++++++++++
tests/test_ui.py | 2 +-
2 files changed, 12 insertions(+), 1 deletion(-)
create mode 100644 src/leibnetz/ui/__main__.py
diff --git a/src/leibnetz/ui/__main__.py b/src/leibnetz/ui/__main__.py
new file mode 100644
index 0000000..3c533ce
--- /dev/null
+++ b/src/leibnetz/ui/__main__.py
@@ -0,0 +1,11 @@
+"""
+Entry point for running the UI server as a module.
+
+This allows the server to be started with:
+ python -m leibnetz.ui
+"""
+
+from .server import main
+
+if __name__ == "__main__":
+ main()
diff --git a/tests/test_ui.py b/tests/test_ui.py
index 7449c1b..84afb9a 100644
--- a/tests/test_ui.py
+++ b/tests/test_ui.py
@@ -29,7 +29,7 @@ def test_ui_server_starts():
# Start the server in a subprocess
proc = subprocess.Popen(
- ["python", "-m", "leibnetz.ui.server", "--port", str(port), "--no-browser"],
+ ["python", "-m", "leibnetz.ui", "--port", str(port), "--no-browser"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
cwd=Path(__file__).parent.parent,