Spaces:
Running
Running
// Initialize Konva stage | |
const stage = new Konva.Stage({ | |
container: 'container', | |
width: 1000, | |
height: 600 | |
}); | |
const layer = new Konva.Layer(); | |
stage.add(layer); | |
// Store nodes, connections, and parsed data | |
let nodes = []; | |
let connections = []; // { fromNodeId, fromPortId, toNodeId, toPortId } | |
let parsedConnections = []; | |
let selectedPort = null; // { node, portId, type: 'input'|'output' } | |
let disconnectMode = false; | |
// 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; // Store for auto-connect | |
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 = []; | |
layer.draw(); | |
} | |
// Create nodes and connections from parsed data | |
function createNodesFromParsedData(parsedNodes, parsedConnections) { | |
parsedNodes.forEach(nodeData => { | |
const node = createNode( | |
nodeData.x, | |
nodeData.y, | |
nodeData.label, | |
nodeData.type, | |
nodeData.inputs, | |
nodeData.outputs, | |
nodeData.id | |
); | |
nodes.push(node); | |
layer.add(node); | |
}); | |
layer.draw(); | |
saveNodes(); | |
} | |
// Create a node with inputs and outputs | |
function createNode(x, y, label, type, inputs = [], outputs = [], id) { | |
const node = new Konva.Group({ | |
x: x, | |
y: y, | |
draggable: true | |
}); | |
// Node rectangle | |
const color = type === 'function' ? '#ffeb3b' : type.includes('variable') ? '#90caf9' : type === 'import' ? '#a5d6a7' : '#ccc'; | |
const box = new Konva.Rect({ | |
width: 100, | |
height: 50, | |
fill: color, | |
stroke: 'black', | |
strokeWidth: 2, | |
cornerRadius: 5 | |
}); | |
// Node label | |
const text = new Konva.Text({ | |
text: label, | |
fontSize: 12, | |
fontFamily: 'Arial', | |
fill: 'black', | |
width: 100, | |
align: 'center', | |
y: 20 | |
}); | |
node.add(box); | |
node.add(text); | |
// Add input/output ports with unique IDs | |
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: 100, | |
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; // First click on input enables disconnect mode | |
} else if (selectedPort.type === 'output' && selectedPort.node !== node) { | |
// Connect output to input | |
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) { | |
// Select output to disconnect | |
selectedPort = { node, portId: port.id, type: 'input' }; // Keep input selected | |
} 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') { | |
// Disconnect input from output | |
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 | |
}; | |
// Update position and connections on drag | |
node.on('dragmove', () => { | |
node.data.x = node.x(); | |
node.data.y = node.y(); | |
updateConnections(); | |
saveNodes(); | |
}); | |
return node; | |
} | |
// Create a spline (Bezier curve) 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(); | |
// Control points for Bezier curve | |
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(); | |
} | |
// Auto-connect nodes based on parsed connections | |
function autoConnect() { | |
// Clear existing connections | |
layer.find('Shape').forEach(shape => { | |
if (shape.data && shape.data.fromNodeId !== undefined) { | |
shape.destroy(); | |
} | |
}); | |
connections = []; | |
// Create spline connections for parsed data | |
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) { | |
// Find first available output and input ports | |
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 | |
}); | |
} | |
} | |
}); | |
layer.draw(); | |
saveNodes(); | |
} | |
// Add a manual node | |
function addNode() { | |
const node = createNode( | |
Math.random() * (stage.width() - 100), | |
Math.random() * (stage.height() - 100), | |
'Function', | |
'function', | |
['in1'], | |
['out1'], | |
nodes.length | |
); | |
nodes.push(node); | |
layer.add(node); | |
layer.draw(); | |
saveNodes(); | |
} | |
// 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) | |
})), | |
connections: connections | |
}) | |
}).then(response => response.json()) | |
.then(data => console.log('Saved:', data)) | |
.catch(error => console.error('Error:', error)); | |
} | |
// Initial draw | |
layer.draw(); |