From bbcbf5166756aef4017ace331b182e4d4c1c61eb Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 15:31:59 -0400 Subject: [PATCH 1/9] bakend function to read the docs of the class --- src/backend.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/backend.py b/src/backend.py index ab8291c7..8a732b09 100644 --- a/src/backend.py +++ b/src/backend.py @@ -97,6 +97,19 @@ def get_default_values(node_type): ), 400 +@app.route("/get-docs/", methods=["GET"]) +def get_docs(node_type): + try: + if node_type not in map_str_to_object: + return jsonify({"error": f"Unknown node type: {node_type}"}), 400 + + block_class = map_str_to_object[node_type] + docstring = inspect.getdoc(block_class) + return jsonify({"docstring": docstring}) + except Exception as e: + return jsonify({"error": f"Could not get docs for {node_type}: {str(e)}"}), 400 + + # Function to save graphs @app.route("/save", methods=["POST"]) def save_graph(): From 5a3f1fde235ca7a4b27e77b85afd571e336e42e3 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 15:32:17 -0400 Subject: [PATCH 2/9] modified Node sidebar to include documentation --- src/App.jsx | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index 34f85eeb..ec1a2014 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -96,6 +96,7 @@ export default function App() { // Global variables state const [globalVariables, setGlobalVariables] = useState([]); const [defaultValues, setDefaultValues] = useState({}); + const [nodeDocumentation, setNodeDocumentation] = useState({}); // Function to fetch default values for a node type const fetchDefaultValues = async (nodeType) => { @@ -114,6 +115,23 @@ export default function App() { } }; + // Function to fetch documentation for a node type + const fetchNodeDocumentation = async (nodeType) => { + try { + const response = await fetch(getApiEndpoint(`/get-docs/${nodeType}`)); + if (response.ok) { + const result = await response.json(); + return result.docstring || 'No documentation available for this node type.'; + } else { + console.error('Failed to fetch documentation'); + return 'Failed to load documentation.'; + } + } catch (error) { + console.error('Error fetching documentation:', error); + return 'Error loading documentation.'; + } + }; + // Function to save a graph to computer with "Save As" dialog const saveGraph = async () => { const graphData = { @@ -543,7 +561,7 @@ export default function App() { [edges, setEdges] ); // Function that when we click on a node, sets that node as the selected node - const onNodeClick = (event, node) => { + const onNodeClick = async (event, node) => { setSelectedNode(node); setSelectedEdge(null); // Clear selected edge when selecting a node // Reset all edge styles when selecting a node @@ -561,6 +579,17 @@ export default function App() { }, })) ); + + // Fetch default values and documentation for this node type + if (node.type && !defaultValues[node.type]) { + const defaults = await fetchDefaultValues(node.type); + setDefaultValues(prev => ({ ...prev, [node.type]: defaults })); + } + + if (node.type && !nodeDocumentation[node.type]) { + const docs = await fetchNodeDocumentation(node.type); + setNodeDocumentation(prev => ({ ...prev, [node.type]: docs })); + } }; // Function that when we click on an edge, sets that edge as the selected edge const onEdgeClick = (event, edge) => { @@ -632,15 +661,21 @@ export default function App() { const selectedType = availableTypes[choiceIndex]; const newNodeId = nodeCounter.toString(); - // Fetch default values for this node type + // Fetch default values and documentation for this node type const defaults = await fetchDefaultValues(selectedType); + const docs = await fetchNodeDocumentation(selectedType); - // Store default values for this node type + // Store default values and documentation for this node type setDefaultValues(prev => ({ ...prev, [selectedType]: defaults })); + setNodeDocumentation(prev => ({ + ...prev, + [selectedType]: docs + })); + // Create node data with label and initialize all expected fields as empty strings let nodeData = { label: `${selectedType} ${newNodeId}` }; @@ -1174,6 +1209,38 @@ export default function App() { > Close + + {/* Documentation Section */} +
+

+ Class Documentation +

+
+ {nodeDocumentation[selectedNode.type] || 'Loading documentation...'} +
+
)} {selectedEdge && ( From f99c9ef3986c758af8e6e21ee57d87ef2273b5fd Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 15:46:02 -0400 Subject: [PATCH 3/9] first render of docstrings --- src/App.css | 160 +++++++++++++++++++++++++++++++++++++++++++++++++ src/App.jsx | 49 +++++++++------ src/backend.py | 66 +++++++++++++++++++- 3 files changed, 255 insertions(+), 20 deletions(-) diff --git a/src/App.css b/src/App.css index bc1eee66..72d79dbe 100644 --- a/src/App.css +++ b/src/App.css @@ -92,3 +92,163 @@ font-size: 12px; color: #666; } + +/* Documentation HTML rendering styles for dark theme */ +.documentation-content { + color: #e8e8e8; +} + +.documentation-content p { + margin: 0.5em 0; + line-height: 1.4; +} + +.documentation-content h1, +.documentation-content h2, +.documentation-content h3, +.documentation-content h4, +.documentation-content h5, +.documentation-content h6 { + color: #ffffff; + margin: 1em 0 0.5em 0; + font-weight: bold; +} + +.documentation-content h2 { + font-size: 1.1em; + border-bottom: 1px solid #555; + padding-bottom: 0.2em; +} + +.documentation-content h3 { + font-size: 1.05em; +} + +.documentation-content pre { + background-color: #1a1a2e; + border: 1px solid #444; + border-radius: 3px; + padding: 0.8em; + overflow-x: auto; + font-family: 'Courier New', Consolas, monospace; + font-size: 0.9em; + margin: 0.5em 0; +} + +.documentation-content code { + background-color: #1a1a2e; + padding: 0.1em 0.3em; + border-radius: 2px; + font-family: 'Courier New', Consolas, monospace; + font-size: 0.9em; + color: #ffd700; +} + +.documentation-content pre code { + background-color: transparent; + padding: 0; + color: #e8e8e8; +} + +.documentation-content ul, +.documentation-content ol { + margin: 0.5em 0; + padding-left: 1.5em; +} + +.documentation-content li { + margin: 0.2em 0; +} + +.documentation-content blockquote { + border-left: 3px solid #555; + padding-left: 1em; + margin: 0.5em 0; + font-style: italic; + color: #ccc; +} + +.documentation-content table { + border-collapse: collapse; + width: 100%; + margin: 0.5em 0; +} + +.documentation-content th, +.documentation-content td { + border: 1px solid #555; + padding: 0.4em; + text-align: left; +} + +.documentation-content th { + background-color: #333; + font-weight: bold; +} + +.documentation-content em { + font-style: italic; + color: #ddd; +} + +.documentation-content strong { + font-weight: bold; + color: #ffffff; +} + +.documentation-content a { + color: #78A083; + text-decoration: underline; +} + +.documentation-content a:hover { + color: #9bc49f; +} + +/* Docutils-specific classes */ +.documentation-content .field-list { + margin: 0.5em 0; +} + +.documentation-content .field-name { + font-weight: bold; + color: #ffffff; +} + +.documentation-content .field-body { + margin-left: 1em; +} + +.documentation-content .literal { + background-color: #1a1a2e; + padding: 0.1em 0.3em; + border-radius: 2px; + font-family: 'Courier New', Consolas, monospace; + color: #ffd700; +} + +.documentation-content .note, +.documentation-content .warning, +.documentation-content .tip { + background-color: #2a2a3e; + border-left: 4px solid #78A083; + padding: 0.8em; + margin: 0.5em 0; + border-radius: 0 3px 3px 0; +} + +.documentation-content .warning { + border-left-color: #e74c3c; +} + +.documentation-content .note .first, +.documentation-content .warning .first, +.documentation-content .tip .first { + margin-top: 0; +} + +.documentation-content .note .last, +.documentation-content .warning .last, +.documentation-content .tip .last { + margin-bottom: 0; +} diff --git a/src/App.jsx b/src/App.jsx index ec1a2014..b019af38 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -121,14 +121,23 @@ export default function App() { const response = await fetch(getApiEndpoint(`/get-docs/${nodeType}`)); if (response.ok) { const result = await response.json(); - return result.docstring || 'No documentation available for this node type.'; + return { + html: result.html || result.docstring || 'No documentation available for this node type.', + text: result.docstring || 'No documentation available for this node type.' + }; } else { console.error('Failed to fetch documentation'); - return 'Failed to load documentation.'; + return { + html: '

Failed to load documentation.

', + text: 'Failed to load documentation.' + }; } } catch (error) { console.error('Error fetching documentation:', error); - return 'Error loading documentation.'; + return { + html: '

Error loading documentation.

', + text: 'Error loading documentation.' + }; } }; @@ -1224,22 +1233,24 @@ export default function App() { }}> Class Documentation -
- {nodeDocumentation[selectedNode.type] || 'Loading documentation...'} -
+
)} diff --git a/src/backend.py b/src/backend.py index 8a732b09..52dce05a 100644 --- a/src/backend.py +++ b/src/backend.py @@ -13,6 +13,57 @@ from .pathsim_utils import make_pathsim_model, map_str_to_object from pathsim.blocks import Scope +# Sphinx imports for docstring processing +try: + from docutils.core import publish_parts + + SPHINX_AVAILABLE = True +except ImportError: + SPHINX_AVAILABLE = False + print("Warning: Sphinx not available. Docstring formatting will be basic.") + + +def docstring_to_html(docstring): + """Convert a Python docstring to HTML using docutils (like Sphinx does).""" + if not docstring: + return "

No documentation available.

" + + if not SPHINX_AVAILABLE: + # Fallback: simple HTML escaping and line break conversion + import html + + escaped = html.escape(docstring) + return f"
{escaped}
" + + try: + # Use docutils to convert reStructuredText to HTML + # This is similar to what Sphinx does internally + overrides = { + "input_encoding": "utf-8", + "doctitle_xform": False, + "initial_header_level": 2, + } + + parts = publish_parts( + source=docstring, writer_name="html", settings_overrides=overrides + ) + + # Return just the body content (without full HTML document structure) + html_content = parts["body"] + + # Clean up the HTML a bit for better display in the sidebar + html_content = html_content.replace('
', "
") + + return html_content + + except Exception as e: + # Fallback in case of any parsing errors + import html + + escaped = html.escape(docstring) + return f"
Error parsing docstring: {str(e)}\n\n{escaped}
" + + # Configure Flask app for Cloud Run app = Flask(__name__, static_folder="../dist", static_url_path="") @@ -105,7 +156,20 @@ def get_docs(node_type): block_class = map_str_to_object[node_type] docstring = inspect.getdoc(block_class) - return jsonify({"docstring": docstring}) + + # If no docstring, provide a basic description + if not docstring: + docstring = f"No documentation available for {node_type}.\n\nThis is a {node_type} block in the pathsim library." + + # Convert docstring to HTML using docutils/Sphinx-style processing + html_content = docstring_to_html(docstring) + + return jsonify( + { + "docstring": docstring, # Keep original for backwards compatibility + "html": html_content, # New HTML version + } + ) except Exception as e: return jsonify({"error": f"Could not get docs for {node_type}: {str(e)}"}), 400 From 6ae4b451f408dd3c96c1e693676bd81328b1680c Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 15:50:26 -0400 Subject: [PATCH 4/9] collapsible --- src/App.jsx | 73 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b019af38..912be70e 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -97,6 +97,7 @@ export default function App() { const [globalVariables, setGlobalVariables] = useState([]); const [defaultValues, setDefaultValues] = useState({}); const [nodeDocumentation, setNodeDocumentation] = useState({}); + const [isDocumentationExpanded, setIsDocumentationExpanded] = useState(false); // Function to fetch default values for a node type const fetchDefaultValues = async (nodeType) => { @@ -1225,32 +1226,58 @@ export default function App() { borderTop: '1px solid #555', paddingTop: '15px' }}> -

- Class Documentation -

+ onClick={() => setIsDocumentationExpanded(!isDocumentationExpanded)} + > +

+ Class Documentation +

+ + ▶ + +
+ + {isDocumentationExpanded && ( +
+ )}
)} From 0fc75ab43e96da9936135cb505f34e36bb74c880 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 15:58:07 -0400 Subject: [PATCH 5/9] now the side bar is scrollable --- src/App.css | 30 ++++++++++++++++++++++++++++++ src/App.jsx | 21 +++++++++++++++++---- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/App.css b/src/App.css index 72d79dbe..aa11605f 100644 --- a/src/App.css +++ b/src/App.css @@ -252,3 +252,33 @@ .documentation-content .tip .last { margin-bottom: 0; } + +/* Custom scrollbar styles for sidebar */ +.sidebar-scrollable { + scrollbar-width: thin; + scrollbar-color: #555 #1e1e2f; +} + +.sidebar-scrollable::-webkit-scrollbar { + width: 8px; +} + +.sidebar-scrollable::-webkit-scrollbar-track { + background: #1e1e2f; + border-radius: 4px; +} + +.sidebar-scrollable::-webkit-scrollbar-thumb { + background: #555; + border-radius: 4px; + border: 1px solid #1e1e2f; +} + +.sidebar-scrollable::-webkit-scrollbar-thumb:hover { + background: #666; +} + +/* Smooth scrolling for the sidebar */ +.sidebar-scrollable { + scroll-behavior: smooth; +} diff --git a/src/App.jsx b/src/App.jsx index 912be70e..b9714efe 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -927,7 +927,12 @@ export default function App() { {/* Graph Editor Tab */} {activeTab === 'graph' && ( -
+
{selectedNode && (
-

{selectedNode.data.label}

+
+

{selectedNode.data.label}

{(() => { // Get default values for this node type const nodeDefaults = defaultValues[selectedNode.type] || {}; @@ -1279,10 +1287,12 @@ export default function App() { /> )}
+
)} {selectedEdge && (
+

Selected Edge

ID: {selectedEdge.id} @@ -1341,6 +1353,7 @@ export default function App() { > Delete Edge +
)}
From 09228348767a2cb1957f43c6f1419b538f935211 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 16:03:27 -0400 Subject: [PATCH 6/9] fixed main app scrolling --- src/App.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/App.jsx b/src/App.jsx index b9714efe..e9780966 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -929,8 +929,8 @@ export default function App() { {activeTab === 'graph' && (
Date: Sat, 2 Aug 2025 16:08:41 -0400 Subject: [PATCH 7/9] added sphinx to requirements --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d9e63044..7bf93ba3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,4 +10,6 @@ pathsim==0.7.0 matplotlib==3.7.0 numpy==1.24.0 plotly~=6.0 -pytest \ No newline at end of file +pytest +sphinx>=4.0.0 +docutils>=0.17.0 \ No newline at end of file From 11b89ff506e22841c08f1b5698caf31f79dd82a9 Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 16:08:50 -0400 Subject: [PATCH 8/9] fixed controls CSS --- src/App.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/App.css b/src/App.css index aa11605f..a88dada8 100644 --- a/src/App.css +++ b/src/App.css @@ -21,9 +21,7 @@ /* This is the code for customizing the controls icons */ .react-flow__controls { - background-color: #1e1e2f !important; padding: 6px; - min-height: 150px; display: flex; } From 0d1014c7cfa9927ad7d3629d8ecfd17f0babdfee Mon Sep 17 00:00:00 2001 From: RemDelaporteMathurin Date: Sat, 2 Aug 2025 16:21:09 -0400 Subject: [PATCH 9/9] sphinx is not optional + simplified no documentation placeholder --- src/backend.py | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/backend.py b/src/backend.py index 52dce05a..3b26dff9 100644 --- a/src/backend.py +++ b/src/backend.py @@ -14,13 +14,7 @@ from pathsim.blocks import Scope # Sphinx imports for docstring processing -try: - from docutils.core import publish_parts - - SPHINX_AVAILABLE = True -except ImportError: - SPHINX_AVAILABLE = False - print("Warning: Sphinx not available. Docstring formatting will be basic.") +from docutils.core import publish_parts def docstring_to_html(docstring): @@ -28,13 +22,6 @@ def docstring_to_html(docstring): if not docstring: return "

No documentation available.

" - if not SPHINX_AVAILABLE: - # Fallback: simple HTML escaping and line break conversion - import html - - escaped = html.escape(docstring) - return f"
{escaped}
" - try: # Use docutils to convert reStructuredText to HTML # This is similar to what Sphinx does internally @@ -159,7 +146,7 @@ def get_docs(node_type): # If no docstring, provide a basic description if not docstring: - docstring = f"No documentation available for {node_type}.\n\nThis is a {node_type} block in the pathsim library." + docstring = f"No documentation available for {node_type}." # Convert docstring to HTML using docutils/Sphinx-style processing html_content = docstring_to_html(docstring)