Spaces:
Paused
Paused
| // Initialize Konva stage | |
| const stage = new Konva.Stage({ | |
| container: 'container', | |
| width: 1000, | |
| height: 600, | |
| draggable: true | |
| }); | |
| const layer = new Konva.Layer(); | |
| stage.add(layer); | |
| // Store nodes, connections, and parsed data | |
| let nodes = []; | |
| let connections = []; | |
| let parsedConnections = []; | |
| let selectedPort = null; | |
| let disconnectMode = false; | |
| let codeWindow = null; // Track open code window | |
| let codeTextarea = null; // Track textarea element | |
| // Zoom functionality | |
| let scale = 1; | |
| const scaleFactor = 1.1; | |
| const minScale = 0.5; | |
| const maxScale = 2.0; | |
| stage.on('wheel', (e) => { | |
| e.evt.preventDefault(); | |
| const oldScale = scale; | |
| const pointer = stage.getPointerPosition(); | |
| const delta = e.evt.deltaY > 0 ? 1 / scaleFactor : scaleFactor; | |
| const newScale = Math.max(minScale, Math.min(maxScale, oldScale * delta)); | |
| if (newScale === scale) return; | |
| scale = newScale; | |
| const mousePointTo = { | |
| x: (pointer.x - stage.x()) / oldScale, | |
| y: (pointer.y - stage.y()) / oldScale | |
| }; | |
| stage.scale({ x: scale, y: scale }); | |
| const newPos = { | |
| x: pointer.x - mousePointTo.x * scale, | |
| y: pointer.y - mousePointTo.y * scale | |
| }; | |
| stage.position(newPos); | |
| stage.batchDraw(); | |
| }); | |
| // Submit code or file for parsing | |
| function submitCode() { | |
| const fileInput = document.getElementById('codeFile'); | |
| const codeInput = document.getElementById('codeInput').value; | |
| const formData = new FormData(); | |
| if (fileInput.files.length > 0) { | |
| formData.append('file', fileInput.files[0]); | |
| } else if (codeInput) { | |
| formData.append('code', codeInput); | |
| } else { | |
| alert('Please upload a file or paste code.'); | |
| return; | |
| } | |
| fetch('/parse_code', { | |
| method: 'POST', | |
| body: formData | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.error) { | |
| alert(data.error); | |
| return; | |
| } | |
| clearCanvas(); | |
| parsedConnections = data.connections; | |
| createNodesFromParsedData(data.nodes, data.connections); | |
| }) | |
| .catch(error => console.error('Error:', error)); | |
| } | |
| // Clear existing nodes and connections | |
| function clearCanvas() { | |
| nodes.forEach(node => node.destroy()); | |
| layer.find('Shape').forEach(shape => shape.destroy()); | |
| nodes = []; | |
| connections = []; | |
| parsedConnections = []; | |
| if (codeWindow) { | |
| codeWindow.destroy(); | |
| codeWindow = null; | |
| } | |
| if (codeTextarea) { | |
| codeTextarea.remove(); | |
| codeTextarea = null; | |
| } | |
| layer.draw(); | |
| } | |
| // Create nodes and connections from parsed data | |
| function createNodesFromParsedData(parsedNodes, parsedConnections) { | |
| const columns = { | |
| imports: { x: 50, y: 50, count: 0 }, // Column for imports | |
| global: { x: 250, y: 50, count: 0 }, // Column for global scope | |
| functions: {} // Columns for functions | |
| }; | |
| // First pass: Assign function nodes to columns | |
| parsedNodes.forEach(nodeData => { | |
| if (nodeData.type === 'function') { | |
| const functionId = nodeData.id || `Function_${Object.keys(columns.functions).length + 1}`; | |
| columns.functions[functionId] = { | |
| x: 450 + Object.keys(columns.functions).length * 200, | |
| y: 50, | |
| count: 0 | |
| }; | |
| } | |
| }); | |
| // Second pass: Create nodes with column-based positioning | |
| parsedNodes.forEach(nodeData => { | |
| const parentPath = nodeData.parent_path || 'global'; | |
| const level = nodeData.level || 0; | |
| let x, y; | |
| if (nodeData.type === 'import') { | |
| // Imports column | |
| x = columns.imports.x; | |
| y = columns.imports.y + columns.imports.count * 80; | |
| columns.imports.count++; | |
| } else if (nodeData.type === 'function') { | |
| // Function column | |
| const functionId = nodeData.id; | |
| x = columns.functions[functionId].x; | |
| y = columns.functions[functionId].y + columns.functions[functionId].count * 80; | |
| columns.functions[functionId].count++; | |
| } else if (parentPath !== 'global' && parentPath.includes('Function')) { | |
| // Child of a function | |
| const functionId = parentPath.split(' -> ')[0]; | |
| if (columns.functions[functionId]) { | |
| x = columns.functions[functionId].x; | |
| y = columns.functions[functionId].y + columns.functions[functionId].count * 80; | |
| columns.functions[functionId].count++; | |
| } else { | |
| // Fallback to global if function not found | |
| x = columns.global.x; | |
| y = columns.global.y + columns.global.count * 80; | |
| columns.global.count++; | |
| } | |
| } else { | |
| // Global scope (non-import, non-function) | |
| x = columns.global.x; | |
| y = columns.global.y + columns.global.count * 80; | |
| columns.global.count++; | |
| } | |
| const node = createNode( | |
| x, | |
| y, | |
| nodeData.label || 'Unnamed', | |
| nodeData.type || 'other', | |
| nodeData.inputs || [], | |
| nodeData.outputs || [], | |
| nodeData.id || `node_${nodes.length}`, | |
| nodeData.source || '', | |
| parentPath, | |
| level | |
| ); | |
| nodes.push(node); | |
| layer.add(node); | |
| }); | |
| layer.draw(); | |
| autoConnect(); | |
| saveNodes(); | |
| } | |
| // Create a node with inputs, outputs, and code segment | |
| function createNode(x, y, label, type, inputs = [], outputs = [], id, source = '', parent_path = 'global', level = 0) { | |
| const node = new Konva.Group({ | |
| x: x, | |
| y: y, | |
| draggable: true | |
| }); | |
| // Node rectangle | |
| const isNumberBox = type === 'number_box'; | |
| const color = isNumberBox ? '#ffcccb' : type === 'function' ? '#ffeb3b' : type.includes('variable') ? '#90caf9' : type === 'import' ? '#a5d6a7' : '#ccc'; | |
| const width = isNumberBox ? 80 : 100; | |
| const height = isNumberBox ? 40 : 50; | |
| const box = new Konva.Rect({ | |
| width: width, | |
| height: height, | |
| fill: color, | |
| stroke: 'black', | |
| strokeWidth: 2, | |
| cornerRadius: 5 | |
| }); | |
| // Node label | |
| const text = new Konva.Text({ | |
| text: label, | |
| fontSize: 12, | |
| fontFamily: 'Arial', | |
| fill: 'black', | |
| width: width, | |
| align: 'center', | |
| y: isNumberBox ? 14 : 20 | |
| }); | |
| node.add(box); | |
| node.add(text); | |
| // Input/output ports | |
| const inputPorts = inputs.map((input, i) => ({ | |
| id: `input-${id}-${i}`, | |
| name: input, | |
| circle: new Konva.Circle({ | |
| x: 0, | |
| y: 10 + i * 20, | |
| radius: 5, | |
| fill: 'red' | |
| }) | |
| })); | |
| const outputPorts = outputs.map((output, i) => ({ | |
| id: `output-${id}-${i}`, | |
| name: output, | |
| circle: new Konva.Circle({ | |
| x: width, | |
| y: 10 + i * 20, | |
| radius: 5, | |
| fill: 'green' | |
| }) | |
| })); | |
| // Add ports to node and set up click handlers | |
| inputPorts.forEach(port => { | |
| node.add(port.circle); | |
| port.circle.on('click', () => { | |
| if (!selectedPort) { | |
| selectedPort = { node, portId: port.id, type: 'input' }; | |
| disconnectMode = true; | |
| } else if (selectedPort.type === 'output' && selectedPort.node !== node) { | |
| createSplineConnection( | |
| selectedPort.node, | |
| selectedPort.portId, | |
| node, | |
| port.id | |
| ); | |
| connections.push({ | |
| fromNodeId: selectedPort.node.data.id, | |
| fromPortId: selectedPort.portId, | |
| toNodeId: node.data.id, | |
| toPortId: port.id | |
| }); | |
| selectedPort = null; | |
| disconnectMode = false; | |
| saveNodes(); | |
| } else if (disconnectMode && selectedPort.type === 'input' && selectedPort.node === node) { | |
| selectedPort = { node, portId: port.id, type: 'input' }; | |
| } else { | |
| selectedPort = null; | |
| disconnectMode = false; | |
| } | |
| }); | |
| }); | |
| outputPorts.forEach(port => { | |
| node.add(port.circle); | |
| port.circle.on('click', () => { | |
| if (!selectedPort) { | |
| selectedPort = { node, portId: port.id, type: 'output' }; | |
| disconnectMode = false; | |
| } else if (disconnectMode && selectedPort.type === 'input') { | |
| const connIndex = connections.findIndex( | |
| c => c.toNodeId === selectedPort.node.data.id && | |
| c.toPortId === selectedPort.portId && | |
| c.fromNodeId === node.data.id && | |
| c.fromPortId === port.id | |
| ); | |
| if (connIndex !== -1) { | |
| const conn = connections[connIndex]; | |
| const spline = layer.find('Shape').find(s => | |
| s.data.fromNodeId === conn.fromNodeId && | |
| s.data.fromPortId === conn.fromPortId && | |
| s.data.toNodeId === conn.toNodeId && | |
| s.data.toPortId === conn.toPortId | |
| ); | |
| if (spline) spline.destroy(); | |
| connections.splice(connIndex, 1); | |
| layer.draw(); | |
| saveNodes(); | |
| } | |
| selectedPort = null; | |
| disconnectMode = false; | |
| } else { | |
| selectedPort = null; | |
| disconnectMode = false; | |
| } | |
| }); | |
| }); | |
| // Node data | |
| node.data = { | |
| id: id, | |
| type: type, | |
| label: label, | |
| inputs: inputPorts, | |
| outputs: outputPorts, | |
| x: x, | |
| y: y, | |
| source: source, | |
| parent_path: parent_path, | |
| level: level | |
| }; | |
| // Click handler to show code window | |
| box.on('click', () => { | |
| if (codeWindow) { | |
| codeWindow.destroy(); | |
| codeWindow = null; | |
| } | |
| if (codeTextarea) { | |
| codeTextarea.remove(); | |
| codeTextarea = null; | |
| } | |
| const nodePos = node.getAbsolutePosition(); | |
| codeWindow = new Konva.Group({ | |
| x: node.x(), | |
| y: node.y() + height + 10 | |
| }); | |
| const codeBox = new Konva.Rect({ | |
| width: 300, | |
| height: 100, | |
| fill: '#f0f0f0', | |
| stroke: 'black', | |
| strokeWidth: 1, | |
| cornerRadius: 5 | |
| }); | |
| // Display source in Konva Text for visual containment | |
| const codeText = new Konva.Text({ | |
| x: 5, | |
| y: 5, | |
| text: source || '', | |
| fontSize: 12, | |
| fontFamily: 'monospace', | |
| fill: 'black', | |
| width: 290, | |
| padding: 5 | |
| }); | |
| codeWindow.add(codeBox); | |
| codeWindow.add(codeText); | |
| // Create textarea for editing | |
| codeTextarea = document.createElement('textarea'); | |
| codeTextarea.style.position = 'relative'; | |
| const canvasRect = stage.container().getBoundingClientRect(); | |
| const textareaX = (nodePos.x + stage.x()) / scale + canvasRect.left; | |
| const textareaY = (nodePos.y + height + 10 + stage.y()) / scale + canvasRect.top; | |
| codeTextarea.style.right = '-20px' || `${textareaX}px`; | |
| codeTextarea.style.top = '20px' || `${textareaY}px`; | |
| codeTextarea.style.width = `${300 / scale}px`; | |
| codeTextarea.style.height = `${100 / scale}px`; | |
| codeTextarea.style.fontFamily = 'monospace'; | |
| codeTextarea.style.fontSize = `${12 / scale}px`; | |
| codeTextarea.style.background = 'transparent'; | |
| codeTextarea.style.border = 'none'; | |
| codeTextarea.style.resize = 'none'; | |
| codeTextarea.value = source || ''; | |
| document.body.appendChild(codeTextarea); | |
| codeWindow.add(codeBox); | |
| codeWindow.add(codeText); | |
| //codeWindow.add(codeTextarea); | |
| layer.add(codeWindow); | |
| // Update code on change | |
| codeTextarea.addEventListener('change', () => { | |
| const newSource = codeTextarea.value; | |
| node.data.source = newSource; | |
| codeText.text(newSource); | |
| fetch('/update_node', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| id: node.data.id, | |
| source: newSource | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.error) { | |
| alert(data.error); | |
| } else { | |
| console.log('Node updated:', data); | |
| updateProgram(); | |
| } | |
| }) | |
| .catch(error => console.error('Error:', error)); | |
| }); | |
| // Close window on click outside | |
| stage.on('click', (e) => { | |
| if (e.target !== box && codeWindow) { | |
| codeWindow.destroy(); | |
| codeWindow = null; | |
| if (codeTextarea) { | |
| codeTextarea.remove(); | |
| codeTextarea = null; | |
| } | |
| stage.off('click'); | |
| } | |
| }); | |
| layer.draw(); | |
| }); | |
| // Update position and connections on drag | |
| node.on('dragmove', () => { | |
| node.data.x = node.x(); | |
| node.data.y = node.y(); | |
| if (codeWindow && codeTextarea) { | |
| const nodePos = node.getAbsolutePosition(); | |
| codeWindow.position({ x: node.x(), y: node.y() + height + 10 }); | |
| const canvasRect = stage.container().getBoundingClientRect(); | |
| const textareaX = (nodePos.x + stage.x()) / scale + canvasRect.left; | |
| const textareaY = (nodePos.y + height + 10 + stage.y()) / scale + canvasRect.top; | |
| codeTextarea.style.left = `${textareaX}px`; | |
| codeTextarea.style.top = `${textareaY}px`; | |
| } | |
| updateConnections(); | |
| saveNodes(); | |
| }); | |
| return node; | |
| } | |
| // Create a spline connection | |
| function createSplineConnection(fromNode, fromPortId, toNode, toPortId) { | |
| const fromPort = fromNode.data.outputs.find(p => p.id === fromPortId); | |
| const toPort = toNode.data.inputs.find(p => p.id === toPortId); | |
| if (!fromPort || !toPort) return; | |
| const startX = fromNode.x() + fromPort.circle.x(); | |
| const startY = fromNode.y() + fromPort.circle.y(); | |
| const endX = toNode.x() + toPort.circle.x(); | |
| const endY = toNode.y() + toPort.circle.y(); | |
| const control1X = startX + (endX - startX) / 3; | |
| const control1Y = startY; | |
| const control2X = startX + 2 * (endX - startX) / 3; | |
| const control2Y = endY; | |
| const spline = new Konva.Shape({ | |
| sceneFunc: function(context, shape) { | |
| context.beginPath(); | |
| context.moveTo(startX, startY); | |
| context.bezierCurveTo(control1X, control1Y, control2X, control2Y, endX, endY); | |
| context.fillStrokeShape(shape); | |
| }, | |
| stroke: 'black', | |
| strokeWidth: 2 | |
| }); | |
| spline.data = { | |
| fromNodeId: fromNode.data.id, | |
| fromPortId: fromPortId, | |
| toNodeId: toNode.data.id, | |
| toPortId: toPortId | |
| }; | |
| layer.add(spline); | |
| layer.draw(); | |
| } | |
| // Enhanced auto-connect based on hierarchy, position, and role | |
| function autoConnect() { | |
| layer.find('Shape').forEach(shape => { | |
| if (shape.data && shape.data.fromNodeId !== undefined) { | |
| shape.destroy(); | |
| } | |
| }); | |
| connections = []; | |
| const sortedNodes = [...nodes].sort((a, b) => { | |
| if (a.data.level !== b.data.level) return a.data.level - b.data.level; | |
| return a.data.y - b.data.y; // Sort by y within columns | |
| }); | |
| const hierarchy = {}; | |
| sortedNodes.forEach(node => { | |
| const parent = node.data.parent_path.split(' -> ')[0] || 'global'; | |
| if (!hierarchy[parent]) hierarchy[parent] = []; | |
| hierarchy[parent].push(node); | |
| }); | |
| parsedConnections.forEach(conn => { | |
| const fromNode = nodes.find(n => n.data.id === conn.from); | |
| const toNode = nodes.find(n => n.data.id === conn.to); | |
| if (fromNode && toNode) { | |
| const fromPort = fromNode.data.outputs[0]; | |
| const toPort = toNode.data.inputs[0]; | |
| if (fromPort && toPort) { | |
| createSplineConnection(fromNode, fromPort.id, toNode, toPort.id); | |
| connections.push({ | |
| fromNodeId: fromNode.data.id, | |
| fromPortId: fromPort.id, | |
| toNodeId: toNode.data.id, | |
| toPortId: toPort.id | |
| }); | |
| } | |
| } | |
| }); | |
| sortedNodes.forEach(node => { | |
| const nodeId = node.data.id; | |
| const parent = node.data.parent_path.split(' -> ')[0] || 'global'; | |
| const role = node.data.type; | |
| if (parent !== 'global') { | |
| const parentNode = nodes.find(n => n.data.id === parent || n.data.label === parent.split('[')[0]); | |
| if (parentNode && parentNode.data.outputs.length > 0 && node.data.inputs.length > 0) { | |
| const fromPort = parentNode.data.outputs[0]; | |
| const toPort = node.data.inputs[0]; | |
| if (!connections.some(c => c.fromNodeId === parentNode.data.id && c.toNodeId === nodeId)) { | |
| createSplineConnection(parentNode, fromPort.id, node, toPort.id); | |
| connections.push({ | |
| fromNodeId: parentNode.data.id, | |
| fromPortId: fromPort.id, | |
| toNodeId: nodeId, | |
| toPortId: toPort.id | |
| }); | |
| } | |
| } | |
| } | |
| if (role.includes('variable')) { | |
| const varName = node.data.label; | |
| sortedNodes.forEach(otherNode => { | |
| if (otherNode !== node && otherNode.data.source.includes(varName)) { | |
| const fromPort = node.data.outputs[0]; | |
| const toPort = otherNode.data.inputs[0]; | |
| if (fromPort && toPort && !connections.some(c => c.fromNodeId === nodeId && c.toNodeId === otherNode.data.id)) { | |
| createSplineConnection(node, fromPort.id, otherNode, toPort.id); | |
| connections.push({ | |
| fromNodeId: nodeId, | |
| fromPortId: fromPort.id, | |
| toNodeId: otherNode.data.id, | |
| toPortId: toPort.id | |
| }); | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| layer.draw(); | |
| saveNodes(); | |
| } | |
| // Update full program | |
| function updateProgram() { | |
| const program = reconstructProgram(); | |
| document.getElementById('codeInput').value = program; | |
| fetch('/update_program', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ code: program }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.error) { | |
| alert(data.error); | |
| } else { | |
| console.log('Program updated:', data); | |
| } | |
| }) | |
| .catch(error => console.error('Error:', error)); | |
| } | |
| // Reconstruct the original program | |
| function reconstructProgram() { | |
| const sortedNodes = [...nodes].sort((a, b) => { | |
| if (a.data.level !== b.data.level) return a.data.level - b.data.level; | |
| return a.data.y - b.data.y; // Sort by y within columns | |
| }); | |
| let program = ''; | |
| sortedNodes.forEach(node => { | |
| const source = node.data.source || ''; | |
| const level = node.data.level || 0; | |
| const indent = ' '.repeat(level); | |
| program += indent + source.trim() + '\n'; | |
| }); | |
| return program.trim(); | |
| } | |
| // Add a manual node | |
| function addNode() { | |
| const node = createNode( | |
| 250, // Add to global column | |
| 50 + nodes.filter(n => n.data.parent_path === 'global').length * 80, | |
| 'Function', | |
| 'function', | |
| ['in1'], | |
| ['out1'], | |
| nodes.length, | |
| 'def new_function(): pass', | |
| 'global', | |
| 0 | |
| ); | |
| nodes.push(node); | |
| layer.add(node); | |
| layer.draw(); | |
| saveNodes(); | |
| } | |
| // Update spline connections when nodes move | |
| function createSplineConnection(fromNode, fromPortId, toNode, toPortId) { | |
| const fromPort = fromNode.data.outputs.find(p => p.id === fromPortId); | |
| const toPort = toNode.data.inputs.find(p => p.id === toPortId); | |
| if (!fromPort || !toPort) return; | |
| const startX = fromNode.x() + fromPort.circle.x(); | |
| const startY = fromNode.y() + fromPort.circle.y(); | |
| const endX = toNode.x() + toPort.circle.x(); | |
| const endY = toNode.y() + toPort.circle.y(); | |
| const control1X = startX + (endX - startX) / 3; | |
| const control1Y = startY; | |
| const control2X = startX + 2 * (endX - startX) / 3; | |
| const control2Y = endY; | |
| const spline = new Konva.Shape({ | |
| sceneFunc: function(context, shape) { | |
| context.beginPath(); | |
| context.moveTo(startX, startY); | |
| context.bezierCurveTo(control1X, control1Y, control2X, control2Y, endX, endY); | |
| context.fillStrokeShape(shape); | |
| }, | |
| stroke: 'black', | |
| strokeWidth: 2 | |
| }); | |
| spline.data = { | |
| fromNodeId: fromNode.data.id, | |
| fromPortId: fromPortId, | |
| toNodeId: toNode.data.id, | |
| toPortId: toPortId | |
| }; | |
| layer.add(spline); | |
| layer.draw(); | |
| } | |
| // Update spline connections when nodes move | |
| function updateConnections() { | |
| layer.find('Shape').forEach(shape => { | |
| if (shape.data && shape.data.fromNodeId !== undefined) { | |
| const fromNode = nodes.find(n => n.data.id === shape.data.fromNodeId); | |
| const toNode = nodes.find(n => n.data.id === shape.data.toNodeId); | |
| if (fromNode && toNode) { | |
| const fromPort = fromNode.data.outputs.find(p => p.id === shape.data.fromPortId); | |
| const toPort = toNode.data.inputs.find(p => p.id === shape.data.toPortId); | |
| if (fromPort && toPort) { | |
| const startX = fromNode.x() + fromPort.circle.x(); | |
| const startY = fromNode.y() + fromPort.circle.y(); | |
| const endX = toNode.x() + toPort.circle.x(); | |
| const endY = toNode.y() + toPort.circle.y(); | |
| const control1X = startX + (endX - startX) / 3; | |
| const control1Y = startY; | |
| const control2X = startX + 2 * (endX - startX) / 3; | |
| const control2Y = endY; | |
| shape.sceneFunc(function(context, shape) { | |
| context.beginPath(); | |
| context.moveTo(startX, startY); | |
| context.bezierCurveTo(control1X, control1Y, control2X, control2Y, endX, endY); | |
| context.fillStrokeShape(shape); | |
| }); | |
| } | |
| } | |
| } | |
| }); | |
| layer.draw(); | |
| } | |
| // Save nodes and connections to backend | |
| function saveNodes() { | |
| fetch('/save_nodes', { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| nodes: nodes.map(n => ({ | |
| id: n.data.id, | |
| type: n.data.type, | |
| label: n.data.label, | |
| x: n.data.x, | |
| y: n.data.y, | |
| inputs: n.data.inputs.map(p => p.name), | |
| outputs: n.data.outputs.map(p => p.name), | |
| source: n.data.source, | |
| parent_path: n.data.parent_path, | |
| level: n.data.level | |
| })), | |
| connections: connections | |
| }) | |
| }) | |
| .then(response => response.json()) | |
| .then(data => console.log('Saved:', data)) | |
| .catch(error => console.error('Error:', error)); | |
| } | |
| // Initial draw | |
| layer.draw(); |