From 6ac0fae6b2877768164a68b1d5e12ac2ba7c4c97 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 4 Aug 2025 14:10:52 -0400 Subject: [PATCH 1/6] added node id and type + improve label editing --- src/App.jsx | 99 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 62aea125..ae68e26a 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -78,6 +78,8 @@ const DnDFlow = () => { // Global variables state const [globalVariables, setGlobalVariables] = useState([]); const [defaultValues, setDefaultValues] = useState({}); + const [isEditingLabel, setIsEditingLabel] = useState(false); + const [tempLabel, setTempLabel] = useState(''); const [nodeDocumentation, setNodeDocumentation] = useState({}); const [isDocumentationExpanded, setIsDocumentationExpanded] = useState(false); @@ -1230,15 +1232,106 @@ const DnDFlow = () => { }} >
-

{selectedNode.data.label}

+ {isEditingLabel ? ( + setTempLabel(e.target.value)} + onBlur={() => { + // Update the node label + const updatedNode = { + ...selectedNode, + data: { ...selectedNode.data, label: tempLabel }, + }; + setNodes((nds) => + nds.map((node) => + node.id === selectedNode.id ? updatedNode : node + ) + ); + setSelectedNode(updatedNode); + setIsEditingLabel(false); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.target.blur(); // This will trigger onBlur + } + if (e.key === 'Escape') { + setTempLabel(selectedNode.data.label); // Reset to original + setIsEditingLabel(false); + } + }} + autoFocus + style={{ + fontSize: '24px', + fontWeight: 'bold', + color: '#ffffff', + backgroundColor: '#2a2a3e', + border: '2px solid #007bff', + borderRadius: '4px', + padding: '8px 12px', + width: '100%', + marginBottom: '16px', + outline: 'none' + }} + /> + ) : ( +

{ + setTempLabel(selectedNode.data.label); + setIsEditingLabel(true); + }} + style={{ + cursor: 'pointer', + margin: '0 0 16px 0', + padding: '8px 12px', + borderRadius: '4px', + transition: 'background-color 0.2s ease', + backgroundColor: 'transparent', + border: '2px solid transparent' + }} + onMouseEnter={(e) => { + e.target.style.backgroundColor = '#2a2a3e'; + e.target.style.borderColor = '#444'; + }} + onMouseLeave={(e) => { + e.target.style.backgroundColor = 'transparent'; + e.target.style.borderColor = 'transparent'; + }} + title="Click to edit label" + > + {selectedNode.data.label} +

+ )} +

TYPE: {selectedNode.type}

+

ID: {selectedNode.id}

+ {(() => { // Get default values for this node type const nodeDefaults = defaultValues[selectedNode.type] || {}; // Create a list of all possible parameters (both current data and defaults) + // Exclude 'label' since it's now editable directly in the title const allParams = new Set([ - ...Object.keys(selectedNode.data), - ...Object.keys(nodeDefaults) + ...Object.keys(selectedNode.data).filter(key => key !== 'label'), + ...Object.keys(nodeDefaults).filter(key => key !== 'label') ]); return Array.from(allParams) From e593b8508abd1c908ade1f7f02cedfbb90350f04 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 4 Aug 2025 14:21:20 -0400 Subject: [PATCH 2/6] moved to its own file --- src/App.jsx | 264 +++---------------------------------------- src/NodeSidebar.jsx | 270 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 284 insertions(+), 250 deletions(-) create mode 100644 src/NodeSidebar.jsx diff --git a/src/App.jsx b/src/App.jsx index ae68e26a..42f8dd7c 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -15,6 +15,7 @@ import './App.css'; import Plot from 'react-plotly.js'; import { getApiEndpoint } from './config.js'; import Sidebar from './Sidebar'; +import NodeSidebar from './NodeSidebar'; import { DnDProvider, useDnD } from './DnDContext.jsx'; import ContextMenu from './ContextMenu.jsx'; @@ -1213,256 +1214,19 @@ const DnDFlow = () => {
- {selectedNode && ( -
-
- {isEditingLabel ? ( - setTempLabel(e.target.value)} - onBlur={() => { - // Update the node label - const updatedNode = { - ...selectedNode, - data: { ...selectedNode.data, label: tempLabel }, - }; - setNodes((nds) => - nds.map((node) => - node.id === selectedNode.id ? updatedNode : node - ) - ); - setSelectedNode(updatedNode); - setIsEditingLabel(false); - }} - onKeyDown={(e) => { - if (e.key === 'Enter') { - e.target.blur(); // This will trigger onBlur - } - if (e.key === 'Escape') { - setTempLabel(selectedNode.data.label); // Reset to original - setIsEditingLabel(false); - } - }} - autoFocus - style={{ - fontSize: '24px', - fontWeight: 'bold', - color: '#ffffff', - backgroundColor: '#2a2a3e', - border: '2px solid #007bff', - borderRadius: '4px', - padding: '8px 12px', - width: '100%', - marginBottom: '16px', - outline: 'none' - }} - /> - ) : ( -

{ - setTempLabel(selectedNode.data.label); - setIsEditingLabel(true); - }} - style={{ - cursor: 'pointer', - margin: '0 0 16px 0', - padding: '8px 12px', - borderRadius: '4px', - transition: 'background-color 0.2s ease', - backgroundColor: 'transparent', - border: '2px solid transparent' - }} - onMouseEnter={(e) => { - e.target.style.backgroundColor = '#2a2a3e'; - e.target.style.borderColor = '#444'; - }} - onMouseLeave={(e) => { - e.target.style.backgroundColor = 'transparent'; - e.target.style.borderColor = 'transparent'; - }} - title="Click to edit label" - > - {selectedNode.data.label} -

- )} -

TYPE: {selectedNode.type}

-

ID: {selectedNode.id}

- - {(() => { - // Get default values for this node type - const nodeDefaults = defaultValues[selectedNode.type] || {}; - - // Create a list of all possible parameters (both current data and defaults) - // Exclude 'label' since it's now editable directly in the title - const allParams = new Set([ - ...Object.keys(selectedNode.data).filter(key => key !== 'label'), - ...Object.keys(nodeDefaults).filter(key => key !== 'label') - ]); - - return Array.from(allParams) - .map(key => { - const currentValue = selectedNode.data[key] || ''; - const defaultValue = nodeDefaults[key]; - const placeholder = defaultValue !== undefined && defaultValue !== null ? - String(defaultValue) : ''; - - return ( -
- - { - const newValue = e.target.value; - const updatedNode = { - ...selectedNode, - data: { ...selectedNode.data, [key]: newValue }, - }; - - setNodes((nds) => - nds.map((node) => - node.id === selectedNode.id ? updatedNode : node - ) - ); - setSelectedNode(updatedNode); - }} - style={{ - width: '100%', - marginTop: 4, - padding: '8px', - borderRadius: '4px', - border: '1px solid #555', - backgroundColor: '#2a2a3e', - color: '#ffffff', - fontSize: '14px' - }} - /> -
- ); - }); - })()} - -
- - - {/* Documentation Section */} -
-
setIsDocumentationExpanded(!isDocumentationExpanded)} - > -

- Class Documentation -

- - ▶ - -
- - {isDocumentationExpanded && ( -
- )} -
-
-
- )} + {selectedEdge && (
{ + if (!selectedNode) return null; + + return ( +
+
+ {isEditingLabel ? ( + setTempLabel(e.target.value)} + onBlur={() => { + // Update the node label + const updatedNode = { + ...selectedNode, + data: { ...selectedNode.data, label: tempLabel }, + }; + setNodes((nds) => + nds.map((node) => + node.id === selectedNode.id ? updatedNode : node + ) + ); + setSelectedNode(updatedNode); + setIsEditingLabel(false); + }} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.target.blur(); // This will trigger onBlur + } + if (e.key === 'Escape') { + setTempLabel(selectedNode.data.label); // Reset to original + setIsEditingLabel(false); + } + }} + autoFocus + style={{ + fontSize: '24px', + fontWeight: 'bold', + color: '#ffffff', + backgroundColor: '#2a2a3e', + border: '2px solid #007bff', + borderRadius: '4px', + padding: '8px 12px', + width: '100%', + marginBottom: '16px', + outline: 'none' + }} + /> + ) : ( +

{ + setTempLabel(selectedNode.data.label); + setIsEditingLabel(true); + }} + style={{ + cursor: 'pointer', + margin: '0 0 16px 0', + padding: '8px 12px', + borderRadius: '4px', + transition: 'background-color 0.2s ease', + backgroundColor: 'transparent', + border: '2px solid transparent' + }} + onMouseEnter={(e) => { + e.target.style.backgroundColor = '#2a2a3e'; + e.target.style.borderColor = '#444'; + }} + onMouseLeave={(e) => { + e.target.style.backgroundColor = 'transparent'; + e.target.style.borderColor = 'transparent'; + }} + title="Click to edit label" + > + {selectedNode.data.label} +

+ )} +

TYPE: {selectedNode.type}

+

ID: {selectedNode.id}

+ + {(() => { + // Get default values for this node type + const nodeDefaults = defaultValues[selectedNode.type] || {}; + + // Create a list of all possible parameters (both current data and defaults) + // Exclude 'label' since it's now editable directly in the title + const allParams = new Set([ + ...Object.keys(selectedNode.data).filter(key => key !== 'label'), + ...Object.keys(nodeDefaults).filter(key => key !== 'label') + ]); + + return Array.from(allParams) + .map(key => { + const currentValue = selectedNode.data[key] || ''; + const defaultValue = nodeDefaults[key]; + const placeholder = defaultValue !== undefined && defaultValue !== null ? + String(defaultValue) : ''; + + return ( +
+ + { + const newValue = e.target.value; + const updatedNode = { + ...selectedNode, + data: { ...selectedNode.data, [key]: newValue }, + }; + + setNodes((nds) => + nds.map((node) => + node.id === selectedNode.id ? updatedNode : node + ) + ); + setSelectedNode(updatedNode); + }} + style={{ + width: '100%', + marginTop: 4, + padding: '8px', + borderRadius: '4px', + border: '1px solid #555', + backgroundColor: '#2a2a3e', + color: '#ffffff', + fontSize: '14px' + }} + /> +
+ ); + }); + })()} + +
+ + + {/* Documentation Section */} +
+
setIsDocumentationExpanded(!isDocumentationExpanded)} + > +

+ Class Documentation +

+ + ▶ + +
+ + {isDocumentationExpanded && ( +
+ )} +
+
+
+ ); +}; + +export default NodeSidebar; From 4ced013c1c96e5ae744b0810ad2de96374c6dc2f Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 4 Aug 2025 14:22:56 -0400 Subject: [PATCH 3/6] comment --- src/App.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/App.jsx b/src/App.jsx index 42f8dd7c..6d48da76 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1860,6 +1860,7 @@ const DnDFlow = () => {
)} + {/* Results Tab */} {activeTab === 'results' && (
Date: Mon, 4 Aug 2025 14:27:03 -0400 Subject: [PATCH 4/6] make_var_name --- src/convert_to_python.py | 52 ++++++++++++++++++++----------------- test/test_convert_python.py | 2 +- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 5d4e1a33..0a3781f7 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -29,7 +29,32 @@ def convert_graph_to_python(graph_data: dict) -> str: return template.render(context) -def process_node_data(nodes: list[dict], edges: list[dict]) -> list[dict]: +def make_var_name(node: dict) -> str: + """ + Create a variable name from the node label, ensuring it is a valid Python identifier. + If the label contains invalid characters, they are replaced with underscores. + If the variable name is not unique, a number is appended to make it unique. + """ + # Make a variable name from the label + invalid_chars = set("!@#$%^&*()+=[]{}|;:'\",.-<>?/\\`~") + base_var_name = node["data"]["label"].lower().replace(" ", "_") + for char in invalid_chars: + base_var_name = base_var_name.replace(char, "") + + # Ensure the base variable name is a valid identifier + if not base_var_name.isidentifier(): + raise ValueError( + f"Variable name must be a valid identifier. {node['data']['label']} to {base_var_name}" + ) + + # Make the variable name unique by appending a number if needed + var_name = base_var_name + var_name = f"{base_var_name}_{node['id']}" + + return var_name + + +def process_node_data(nodes: list[dict]) -> list[dict]: """ Given a list of node and edge data as dictionaries, process the nodes to create variable names, class names, and expected arguments for each node. @@ -38,30 +63,9 @@ def process_node_data(nodes: list[dict], edges: list[dict]) -> list[dict]: The processed node data with variable names, class names, and expected arguments. """ nodes = nodes.copy() - used_var_names = set() for node in nodes: - # Make a variable name from the label - invalid_chars = set("!@#$%^&*()+=[]{}|;:'\",.-<>?/\\`~") - base_var_name = node["data"]["label"].lower().replace(" ", "_") - for char in invalid_chars: - base_var_name = base_var_name.replace(char, "") - - # Ensure the base variable name is a valid identifier - if not base_var_name.isidentifier(): - raise ValueError( - f"Variable name must be a valid identifier. {node['data']['label']} to {base_var_name}" - ) - - # Make the variable name unique by appending a number if needed - var_name = base_var_name - counter = 1 - while var_name in used_var_names: - var_name = f"{base_var_name}_{counter}" - counter += 1 - - node["var_name"] = var_name - used_var_names.add(var_name) + node["var_name"] = make_var_name(node) # Add pathsim class name block_class = map_str_to_object.get(node["type"]) @@ -148,7 +152,7 @@ def process_graph_data_from_dict(data: dict) -> dict: data = data.copy() # Process nodes to create variable names and class names - data["nodes"] = process_node_data(data["nodes"], data["edges"]) + data["nodes"] = process_node_data(data["nodes"]) # Process to add source/target variable names to edges + ports data["edges"] = make_edge_data(data) diff --git a/test/test_convert_python.py b/test/test_convert_python.py index f20b9b50..1bd5fa28 100644 --- a/test/test_convert_python.py +++ b/test/test_convert_python.py @@ -138,7 +138,7 @@ def test_bubbler_has_reset_times(): "id": "1", "type": "bubbler", "data": { - "label": "bubbler_1", + "label": "bubbler", "replacement_times": "[10, 20]", "conversion_efficiency": "1", "vial_efficiency": "0.8", From b2b7199bb8b1316e1ddae3860c3b4f3a343e0491 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 4 Aug 2025 14:40:43 -0400 Subject: [PATCH 5/6] added varname to the node info --- src/App.jsx | 26 +------------------------- src/NodeSidebar.jsx | 27 ++++++++++++++++++++++++++- src/convert_to_python.py | 16 ++++++++++------ 3 files changed, 37 insertions(+), 32 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 6d48da76..b2bb8671 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -18,7 +18,7 @@ import Sidebar from './Sidebar'; import NodeSidebar from './NodeSidebar'; import { DnDProvider, useDnD } from './DnDContext.jsx'; import ContextMenu from './ContextMenu.jsx'; - +import { isValidPythonIdentifier } from './utils.js'; import { makeEdge } from './CustomEdge'; import { nodeTypes } from './nodeConfig.js'; @@ -561,30 +561,6 @@ const DnDFlow = () => { } }; - // Functions for managing global variables - const isValidPythonIdentifier = (name) => { - // Check if name is empty - if (!name) return false; - - // Python identifier rules: - // - Must start with letter or underscore - // - Can contain letters, digits, underscores - // - Cannot be a Python keyword - const pythonKeywords = [ - 'False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', - 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', - 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', - 'raise', 'return', 'try', 'while', 'with', 'yield' - ]; - - // Check if it's a keyword - if (pythonKeywords.includes(name)) return false; - - // Check pattern: must start with letter or underscore, followed by letters, digits, or underscores - const pattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; - return pattern.test(name); - }; - const addGlobalVariable = () => { const newVariable = { id: Date.now().toString(), diff --git a/src/NodeSidebar.jsx b/src/NodeSidebar.jsx index 5704e565..5f3fea9c 100644 --- a/src/NodeSidebar.jsx +++ b/src/NodeSidebar.jsx @@ -1,4 +1,20 @@ -import { useState } from 'react'; +import { isValidPythonIdentifier } from './utils.js'; + +const makeVarName = (node) => { + // Create a base variable name from the node label + const baseVarName = node.data.label.replace(/\s+/g, '_').toLowerCase(); + + // Make the variable name unique by appending a number + let varName = baseVarName; + varName = `${baseVarName}_${node.id}`; + + if (!isValidPythonIdentifier(varName)) { + // add var_ prefix if it doesn't start with a letter or underscore + varName = `var_${varName}`; +} + + return varName; +} const NodeSidebar = ({ selectedNode, @@ -124,6 +140,15 @@ const NodeSidebar = ({ borderBottom: '1px solid #343556', paddingBottom: '8px' }}>ID: {selectedNode.id} +

varname: {makeVarName(selectedNode)}

{(() => { // Get default values for this node type diff --git a/src/convert_to_python.py b/src/convert_to_python.py index 0a3781f7..4f039d61 100644 --- a/src/convert_to_python.py +++ b/src/convert_to_python.py @@ -34,6 +34,8 @@ def make_var_name(node: dict) -> str: Create a variable name from the node label, ensuring it is a valid Python identifier. If the label contains invalid characters, they are replaced with underscores. If the variable name is not unique, a number is appended to make it unique. + + This is supposed to match the logic in NodeSidebar.jsx makeVarName function. """ # Make a variable name from the label invalid_chars = set("!@#$%^&*()+=[]{}|;:'\",.-<>?/\\`~") @@ -41,16 +43,18 @@ def make_var_name(node: dict) -> str: for char in invalid_chars: base_var_name = base_var_name.replace(char, "") - # Ensure the base variable name is a valid identifier - if not base_var_name.isidentifier(): - raise ValueError( - f"Variable name must be a valid identifier. {node['data']['label']} to {base_var_name}" - ) - # Make the variable name unique by appending a number if needed var_name = base_var_name var_name = f"{base_var_name}_{node['id']}" + # Ensure the base variable name is a valid identifier + if not var_name.isidentifier(): + var_name = f"var_{var_name}" + if not var_name.isidentifier(): + raise ValueError( + f"Variable name must be a valid identifier. {node['data']['label']} to {var_name}" + ) + return var_name From f8176691afc153e3f6cb5bc5759a653e8131ebe4 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Mon, 4 Aug 2025 14:42:19 -0400 Subject: [PATCH 6/6] added utils.js --- src/utils.js | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/utils.js diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 00000000..e511ffbf --- /dev/null +++ b/src/utils.js @@ -0,0 +1,25 @@ +// Functions for managing global variables +const isValidPythonIdentifier = (name) => { + // Check if name is empty + if (!name) return false; + + // Python identifier rules: + // - Must start with letter or underscore + // - Can contain letters, digits, underscores + // - Cannot be a Python keyword + const pythonKeywords = [ + 'False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', + 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', + 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', + 'raise', 'return', 'try', 'while', 'with', 'yield' + ]; + + // Check if it's a keyword + if (pythonKeywords.includes(name)) return false; + + // Check pattern: must start with letter or underscore, followed by letters, digits, or underscores + const pattern = /^[a-zA-Z_][a-zA-Z0-9_]*$/; + return pattern.test(name); +}; + +export { isValidPythonIdentifier }; \ No newline at end of file