diff --git a/src/App.css b/src/App.css index a88dada8..2e0c56d3 100644 --- a/src/App.css +++ b/src/App.css @@ -280,3 +280,152 @@ .sidebar-scrollable { scroll-behavior: smooth; } +/* Sidebar and drag-and-drop styles */ +.dndflow { + display: flex; + height: 100%; +} + +.reactflow-wrapper { + flex: 1; + height: 100%; +} + +aside { + color: #ffffff; + font-size: 14px; +} + +aside .description { + margin-bottom: 20px; + padding: 10px; + background-color: #2c2c2c; + border-radius: 5px; + color: #ffffff; + text-align: center; + font-size: 12px; +} + +.dndnode { + height: 50px; + padding: 10px; + border: 1px solid #78A083; + border-radius: 5px; + margin-bottom: 10px; + display: flex; + justify-content: center; + align-items: center; + cursor: grab; + background-color: #444; + color: #ffffff; + font-weight: 500; + transition: background-color 0.2s ease; +} + +.dndnode:hover { + background-color: #78A083; +} + +.dndnode.input { + background-color: #3498db; +} + +.dndnode.input:hover { + background-color: #2980b9; +} + +.dndnode.output { + background-color: #e74c3c; +} + +.dndnode.output:hover { + background-color: #c0392b; +} + +/* Ensure draggable node text is always visible */ +.dndnode { + color: #ffffff !important; + font-weight: 500 !important; + text-align: left !important; + line-height: 1.4 !important; + background-color: #2a2a3e !important; + border: 1px solid #555 !important; +} + +/* Override any inherited styles that might hide text */ +.dndnode * { + color: inherit !important; +} + +/* Improve readability */ +.dndnode:hover { + color: #ffffff !important; + background-color: #3a3a4e !important; +} + +.dndnode:active { + color: #ffffff !important; +} + +/* Ensure category descriptions are readable */ +.sidebar-description { + color: #aaaaaa !important; + font-size: 11px !important; + margin-bottom: 8px !important; + font-style: italic !important; +} + +/* Sidebar node category styles */ +.dndnode.math { + border-left: 4px solid #17a2b8 !important; + background-color: #1a2a3a !important; +} + +.dndnode.control { + border-left: 4px solid #6f42c1 !important; + background-color: #2a1f3d !important; +} + +.dndnode.fuel-cycle { + border-left: 4px solid #fd7e14 !important; + background-color: #3a2a1a !important; +} + +.dndnode.input { + border-left: 4px solid #28a745 !important; + background-color: #1a2f1a !important; +} + +.dndnode.output { + border-left: 4px solid #dc3545 !important; + background-color: #3a1a1a !important; +} + +.dndnode.processing { + border-left: 4px solid #007bff !important; + background-color: #1a1a3a !important; +} + +/* Hover effects for category headers */ +.category-header:hover { + background-color: #3c3c64 !important; +} + +/* Scrollbar styling for sidebar */ +aside::-webkit-scrollbar { + width: 6px; +} + +aside::-webkit-scrollbar-track { + background: #2c2c54; + border-radius: 3px; +} + +aside::-webkit-scrollbar-thumb { + background: #555; + border-radius: 3px; +} + +aside::-webkit-scrollbar-thumb:hover { + background: #666; +} diff --git a/src/App.jsx b/src/App.jsx index e9780966..62aea125 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,71 +1,34 @@ -import React, { useState, useCallback, useEffect, useRef } from 'react'; +// * Imports * +import { useState, useCallback, useEffect, useRef } from 'react'; import { ReactFlow, + ReactFlowProvider, + useReactFlow, MiniMap, Controls, Background, useNodesState, useEdgesState, - addEdge, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; import './App.css'; import Plot from 'react-plotly.js'; import { getApiEndpoint } from './config.js'; - +import Sidebar from './Sidebar'; +import { DnDProvider, useDnD } from './DnDContext.jsx'; import ContextMenu from './ContextMenu.jsx'; -// Importing node components -import { ProcessNode, ProcessNodeHorizontal } from './ProcessNode'; -import DelayNode from './DelayNode'; -import SourceNode from './ConstantNode'; -import { AmplifierNode, AmplifierNodeReverse } from './AmplifierNode'; -import IntegratorNode from './IntegratorNode'; -import AdderNode from './AdderNode'; -import ScopeNode from './ScopeNode'; -import StepSourceNode from './StepSourceNode'; -import {createFunctionNode} from './FunctionNode'; -import DefaultNode from './DefaultNode'; import { makeEdge } from './CustomEdge'; -import MultiplierNode from './MultiplierNode'; -import { Splitter2Node, Splitter3Node } from './Splitters'; -import BubblerNode from './BubblerNode'; -import WallNode from './WallNode'; - -// Add nodes as a node type for this script -const nodeTypes = { - process: ProcessNode, - process_horizontal: ProcessNodeHorizontal, - delay: DelayNode, - constant: SourceNode, - source: SourceNode, - stepsource: StepSourceNode, - pulsesource: SourceNode, - amplifier: AmplifierNode, - amplifier_reverse: AmplifierNodeReverse, - integrator: IntegratorNode, - adder: AdderNode, - multiplier: MultiplierNode, - scope: ScopeNode, - function: createFunctionNode(1, 1), // Default FunctionNode with 1 input and 1 output - function2to2: createFunctionNode(2, 2), // FunctionNode with 2 inputs and 2 outputs - rng: DefaultNode, - pid: DefaultNode, - splitter2: Splitter2Node, - splitter3: Splitter3Node, - wall: WallNode, - bubbler: BubblerNode, - white_noise: SourceNode, - pink_noise: SourceNode, - -}; +import { nodeTypes } from './nodeConfig.js'; + +// * Declaring variables * // Defining initial nodes and edges. In the data section, we have label, but also parameters specific to the node. const initialNodes = []; const initialEdges = []; -// Main App component -export default function App() { +// For Drag and Drop functionality +const DnDFlow = () => { // State management for nodes and edges: adds the initial nodes and edges to the graph and handles node selection const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges); @@ -79,6 +42,25 @@ export default function App() { const [copyFeedback, setCopyFeedback] = useState(''); const ref = useRef(null); const [csvData, setCsvData] = useState(null); + const reactFlowWrapper = useRef(null); + // const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes); + // const [edges, setEdges, onEdgesChange] = useEdgesState([]); + const { screenToFlowPosition } = useReactFlow(); + const [type] = useDnD(); + + // const onConnect = useCallback((params) => setEdges((eds) => addEdge(params, eds)), []); + + const onDragOver = useCallback((event) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + }, []); + + const onDragStart = (event, nodeType) => { + setType(nodeType); + event.dataTransfer.setData('text/plain', nodeType); + event.dataTransfer.effectAllowed = 'move'; + }; + // Solver parameters state const [solverParams, setSolverParams] = useState({ @@ -99,12 +81,22 @@ export default function App() { const [nodeDocumentation, setNodeDocumentation] = useState({}); const [isDocumentationExpanded, setIsDocumentationExpanded] = useState(false); - // Function to fetch default values for a node type + // Function to fetch default values for a node type (with caching) const fetchDefaultValues = async (nodeType) => { + // Check if we already have cached values for this node type + if (defaultValues[nodeType]) { + return defaultValues[nodeType]; + } + try { const response = await fetch(getApiEndpoint(`/default-values/${nodeType}`)); if (response.ok) { const defaults = await response.json(); + // Cache the values + setDefaultValues(prev => ({ + ...prev, + [nodeType]: defaults + })); return defaults; } else { console.error('Failed to fetch default values'); @@ -142,6 +134,81 @@ export default function App() { } }; + // Function to preload all default values at startup + const preloadDefaultValues = async () => { + const availableTypes = Object.keys(nodeTypes); + const promises = availableTypes.map(async (nodeType) => { + try { + const response = await fetch(getApiEndpoint(`/default-values/${nodeType}`)); + if (response.ok) { + const defaults = await response.json(); + return { nodeType, defaults }; + } + } catch (error) { + console.warn(`Failed to preload defaults for ${nodeType}:`, error); + } + return { nodeType, defaults: {} }; + }); + + const results = await Promise.all(promises); + const defaultValuesCache = {}; + results.forEach(({ nodeType, defaults }) => { + defaultValuesCache[nodeType] = defaults; + }); + + setDefaultValues(defaultValuesCache); + }; + + // Preload all default values when component mounts + useEffect(() => { + preloadDefaultValues(); + }, []); + + const onDrop = useCallback( + async (event) => { + event.preventDefault(); + + // check if the dropped element is valid + if (!type) { + return; + } + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + const newNodeId = nodeCounter.toString(); + + // Fetch default values for this node type + let defaults = {}; + + try { + defaults = await fetchDefaultValues(type); + } catch (error) { + console.warn(`Failed to fetch default values for ${type}, using empty defaults:`, error); + defaults = {}; + } + + // Create node data with label and initialize all expected fields as empty strings + let nodeData = { label: `${type} ${newNodeId}` }; + + // Initialize all expected parameters as empty strings + Object.keys(defaults).forEach(key => { + nodeData[key] = ''; + }); + + const newNode = { + id: newNodeId, + type: type, + position: position, + data: nodeData, + }; + + setNodes((nds) => [...nds, newNode]); + setNodeCounter((count) => count + 1); + }, + [screenToFlowPosition, type, nodeCounter, fetchDefaultValues, setDefaultValues, setNodes, setNodeCounter], + ); + // Function to save a graph to computer with "Save As" dialog const saveGraph = async () => { const graphData = { @@ -688,12 +755,12 @@ export default function App() { // Create node data with label and initialize all expected fields as empty strings let nodeData = { label: `${selectedType} ${newNodeId}` }; - + // Initialize all expected parameters as empty strings Object.keys(defaults).forEach(key => { nodeData[key] = ''; }); - + const newNode = { id: newNodeId, type: selectedType, @@ -851,15 +918,11 @@ export default function App() { document.removeEventListener('keydown', handleKeyDown); }; }, [selectedEdge, selectedNode, copiedNode, duplicateNode, setCopyFeedback]); - + return ( -
+
{/* Tab Navigation */}
- - - - - {menu && } - {copyFeedback && ( -
+ {/* Sidebar */} +
+ +
+ + {/* Main Graph Area */} +
+
+ - {copyFeedback} -
- )} - - - - - - - - + + + + {menu && } + {copyFeedback && ( +
+ {copyFeedback} +
+ )} + + + + + + + + -
- Keyboard Shortcuts:
- Ctrl+C: Copy selected node
- Ctrl+V: Paste copied node
- Ctrl+D: Duplicate selected node
- Del/Backspace: Delete selection
- Right-click: Context menu + +
+ Keyboard Shortcuts:
+ Ctrl+C: Copy selected node
+ Ctrl+V: Paste copied node
+ Ctrl+D: Duplicate selected node
+ Del/Backspace: Delete selection
+ Right-click: Context menu +
+
- +
{selectedNode && (
{ // Get default values for this node type const nodeDefaults = defaultValues[selectedNode.type] || {}; - + // Create a list of all possible parameters (both current data and defaults) const allParams = new Set([ ...Object.keys(selectedNode.data), ...Object.keys(nodeDefaults) ]); - + return Array.from(allParams) .map(key => { const currentValue = selectedNode.data[key] || ''; const defaultValue = nodeDefaults[key]; - const placeholder = defaultValue !== undefined && defaultValue !== null ? + const placeholder = defaultValue !== undefined && defaultValue !== null ? String(defaultValue) : ''; - + return (
-