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 diff --git a/src/App.css b/src/App.css index bc1eee66..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; } @@ -92,3 +90,193 @@ 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; +} + +/* 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 34f85eeb..e9780966 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -96,6 +96,8 @@ export default function App() { // Global variables state 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) => { @@ -114,6 +116,32 @@ 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 { + 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 { + html: '

Failed to load documentation.

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

Error loading documentation.

', + text: 'Error loading documentation.' + }; + } + }; + // Function to save a graph to computer with "Save As" dialog const saveGraph = async () => { const graphData = { @@ -543,7 +571,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 +589,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 +671,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}` }; @@ -882,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] || {}; @@ -1174,10 +1227,72 @@ export default function App() { > Close + + {/* Documentation Section */} +
+
setIsDocumentationExpanded(!isDocumentationExpanded)} + > +

+ Class Documentation +

+ + ▶ + +
+ + {isDocumentationExpanded && ( +
+ )} +
+
)} {selectedEdge && (
+

Selected Edge

ID: {selectedEdge.id} @@ -1236,6 +1353,7 @@ export default function App() { > Delete Edge +
)}
@@ -1245,7 +1363,7 @@ export default function App() { {activeTab === 'solver' && (
No documentation available.

" + + 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="") @@ -97,6 +135,32 @@ 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) + + # If no docstring, provide a basic description + if not docstring: + docstring = f"No documentation available for {node_type}." + + # 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 + + # Function to save graphs @app.route("/save", methods=["POST"]) def save_graph():