diff --git a/atomizer-dashboard/frontend/package-lock.json b/atomizer-dashboard/frontend/package-lock.json index 59c5facc..915827f9 100644 --- a/atomizer-dashboard/frontend/package-lock.json +++ b/atomizer-dashboard/frontend/package-lock.json @@ -29,13 +29,15 @@ "react-router-dom": "^6.20.0", "react-syntax-highlighter": "^16.1.0", "react-use-websocket": "^4.13.0", + "reactflow": "^11.11.4", "recharts": "^2.10.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "tailwind-merge": "^3.4.0", "three": "^0.181.2", - "xterm": "^5.3.0" + "xterm": "^5.3.0", + "zustand": "^5.0.10" }, "devDependencies": { "@types/react": "^18.2.43", @@ -1468,6 +1470,276 @@ "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==", "license": "MIT" }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/background/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "license": "MIT", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "license": "MIT", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar/node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.23.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", @@ -1869,30 +2141,159 @@ "@babel/types": "^7.28.2" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, "node_modules/@types/d3-array": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", "license": "MIT" }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, "node_modules/@types/d3-format": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz", "integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==", "license": "MIT" }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", @@ -1908,6 +2309,24 @@ "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", "license": "MIT" }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, "node_modules/@types/d3-scale": { "version": "4.0.9", "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", @@ -1923,6 +2342,12 @@ "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", "license": "MIT" }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, "node_modules/@types/d3-shape": { "version": "3.1.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", @@ -1950,6 +2375,25 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1980,6 +2424,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -2854,6 +3304,12 @@ "node": ">= 6" } }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==", + "license": "MIT" + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2989,6 +3445,28 @@ "node": ">=12" } }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-ease": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", @@ -3057,6 +3535,15 @@ "node": ">=12" } }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/d3-shape": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", @@ -3102,6 +3589,41 @@ "node": ">=12" } }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -6300,6 +6822,24 @@ "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "license": "MIT", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -7549,9 +8089,9 @@ } }, "node_modules/zustand": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz", - "integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", + "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/atomizer-dashboard/frontend/package.json b/atomizer-dashboard/frontend/package.json index 2786e5f8..57d95a2f 100644 --- a/atomizer-dashboard/frontend/package.json +++ b/atomizer-dashboard/frontend/package.json @@ -31,13 +31,15 @@ "react-router-dom": "^6.20.0", "react-syntax-highlighter": "^16.1.0", "react-use-websocket": "^4.13.0", + "reactflow": "^11.11.4", "recharts": "^2.10.3", "rehype-katex": "^7.0.1", "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "tailwind-merge": "^3.4.0", "three": "^0.181.2", - "xterm": "^5.3.0" + "xterm": "^5.3.0", + "zustand": "^5.0.10" }, "devDependencies": { "@types/react": "^18.2.43", diff --git a/atomizer-dashboard/frontend/src/App.tsx b/atomizer-dashboard/frontend/src/App.tsx index d1f634a2..a48fc3d9 100644 --- a/atomizer-dashboard/frontend/src/App.tsx +++ b/atomizer-dashboard/frontend/src/App.tsx @@ -8,6 +8,7 @@ import Dashboard from './pages/Dashboard'; import Analysis from './pages/Analysis'; import Insights from './pages/Insights'; import Results from './pages/Results'; +import CanvasView from './pages/CanvasView'; const queryClient = new QueryClient({ defaultOptions: { @@ -27,6 +28,9 @@ function App() { {/* Home page - no sidebar layout */} } /> + {/* Canvas page - full screen, no sidebar */} + } /> + {/* Study pages - with sidebar layout */} }> } /> diff --git a/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx b/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx new file mode 100644 index 00000000..c1a8c59e --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/AtomizerCanvas.tsx @@ -0,0 +1,146 @@ +import { useCallback, useRef, DragEvent } from 'react'; +import ReactFlow, { + Background, + Controls, + MiniMap, + ReactFlowProvider, + ReactFlowInstance, +} from 'reactflow'; +import 'reactflow/dist/style.css'; + +import { nodeTypes } from './nodes'; +import { NodePalette } from './palette/NodePalette'; +import { NodeConfigPanel } from './panels/NodeConfigPanel'; +import { ValidationPanel } from './panels/ValidationPanel'; +import { useCanvasStore } from '../../hooks/useCanvasStore'; +import { NodeType } from '../../lib/canvas/schema'; + +function CanvasFlow() { + const reactFlowWrapper = useRef(null); + const reactFlowInstance = useRef(null); + + const { + nodes, + edges, + selectedNode, + onNodesChange, + onEdgesChange, + onConnect, + addNode, + selectNode, + validation, + validate, + toIntent, + } = useCanvasStore(); + + const onDragOver = useCallback((event: DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + }, []); + + const onDrop = useCallback( + (event: DragEvent) => { + event.preventDefault(); + + const type = event.dataTransfer.getData('application/reactflow') as NodeType; + if (!type || !reactFlowInstance.current || !reactFlowWrapper.current) return; + + const bounds = reactFlowWrapper.current.getBoundingClientRect(); + const position = reactFlowInstance.current.screenToFlowPosition({ + x: event.clientX - bounds.left, + y: event.clientY - bounds.top, + }); + + addNode(type, position); + }, + [addNode] + ); + + const onNodeClick = useCallback( + (_: React.MouseEvent, node: { id: string }) => { + selectNode(node.id); + }, + [selectNode] + ); + + const onPaneClick = useCallback(() => { + selectNode(null); + }, [selectNode]); + + const handleExecute = () => { + const result = validate(); + if (result.valid) { + const intent = toIntent(); + // Send to chat + console.log('Executing intent:', JSON.stringify(intent, null, 2)); + // TODO: Connect to useChat hook + alert('Intent generated! Check console for JSON output.\n\nIn Phase 2, this will be sent to Claude.'); + } + }; + + return ( +
+ {/* Left: Node Palette */} + + + {/* Center: Canvas */} +
+ { reactFlowInstance.current = instance; }} + onDragOver={onDragOver} + onDrop={onDrop} + onNodeClick={onNodeClick} + onPaneClick={onPaneClick} + nodeTypes={nodeTypes} + fitView + > + + + + + + {/* Execute Button */} +
+ + +
+ + {/* Validation Messages */} + {(validation.errors.length > 0 || validation.warnings.length > 0) && ( + + )} +
+ + {/* Right: Config Panel */} + {selectedNode && } +
+ ); +} + +export function AtomizerCanvas() { + return ( + + + + ); +} diff --git a/atomizer-dashboard/frontend/src/components/canvas/index.ts b/atomizer-dashboard/frontend/src/components/canvas/index.ts new file mode 100644 index 00000000..4a14d2f2 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/index.ts @@ -0,0 +1,5 @@ +export { AtomizerCanvas } from './AtomizerCanvas'; +export { NodePalette } from './palette/NodePalette'; +export { NodeConfigPanel } from './panels/NodeConfigPanel'; +export { ValidationPanel } from './panels/ValidationPanel'; +export * from './nodes'; diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/AlgorithmNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/AlgorithmNode.tsx new file mode 100644 index 00000000..474b513a --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/AlgorithmNode.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { AlgorithmNodeData } from '../../../lib/canvas/schema'; + +function AlgorithmNodeComponent(props: NodeProps) { + const { data } = props; + return ( + 🧠} color="text-indigo-600"> + {data.method &&
{data.method}
} + {data.maxTrials && ( +
{data.maxTrials} trials
+ )} +
+ ); +} +export const AlgorithmNode = memo(AlgorithmNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx new file mode 100644 index 00000000..532f77c5 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/BaseNode.tsx @@ -0,0 +1,72 @@ +import { memo, ReactNode } from 'react'; +import { Handle, Position, NodeProps } from 'reactflow'; +import { BaseNodeData } from '../../../lib/canvas/schema'; + +interface BaseNodeProps extends NodeProps { + icon: ReactNode; + color: string; + children?: ReactNode; + inputs?: number; + outputs?: number; +} + +function BaseNodeComponent({ + data, + selected, + icon, + color, + children, + inputs = 1, + outputs = 1, +}: BaseNodeProps) { + return ( +
+ {/* Input handles */} + {inputs > 0 && ( + + )} + + {/* Header */} +
+ {icon} + {data.label} + {!data.configured && ( + ! + )} +
+ + {/* Content */} + {children &&
{children}
} + + {/* Errors */} + {data.errors?.length ? ( +
+ {data.errors[0]} +
+ ) : null} + + {/* Output handles */} + {outputs > 0 && ( + + )} +
+ ); +} + +export const BaseNode = memo(BaseNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ConstraintNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ConstraintNode.tsx new file mode 100644 index 00000000..5efe6cfd --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ConstraintNode.tsx @@ -0,0 +1,19 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { ConstraintNodeData } from '../../../lib/canvas/schema'; + +function ConstraintNodeComponent(props: NodeProps) { + const { data } = props; + return ( + 🚧} color="text-orange-600"> + {data.name &&
{data.name}
} + {data.operator && data.value !== undefined && ( +
+ {data.operator} {data.value} +
+ )} +
+ ); +} +export const ConstraintNode = memo(ConstraintNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/DesignVarNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/DesignVarNode.tsx new file mode 100644 index 00000000..07c5aaef --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/DesignVarNode.tsx @@ -0,0 +1,19 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { DesignVarNodeData } from '../../../lib/canvas/schema'; + +function DesignVarNodeComponent(props: NodeProps) { + const { data } = props; + return ( + 📐} color="text-green-600"> + {data.expressionName &&
{data.expressionName}
} + {data.minValue !== undefined && data.maxValue !== undefined && ( +
+ [{data.minValue} - {data.maxValue}] {data.unit || ''} +
+ )} +
+ ); +} +export const DesignVarNode = memo(DesignVarNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ExtractorNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ExtractorNode.tsx new file mode 100644 index 00000000..b1c5a95b --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ExtractorNode.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { ExtractorNodeData } from '../../../lib/canvas/schema'; + +function ExtractorNodeComponent(props: NodeProps) { + const { data } = props; + return ( + 🔬} color="text-cyan-600"> + {data.extractorName &&
{data.extractorName}
} + {data.extractorId && ( +
{data.extractorId}
+ )} +
+ ); +} +export const ExtractorNode = memo(ExtractorNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ModelNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ModelNode.tsx new file mode 100644 index 00000000..b1749c96 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ModelNode.tsx @@ -0,0 +1,19 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { ModelNodeData } from '../../../lib/canvas/schema'; + +function ModelNodeComponent(props: NodeProps) { + const { data } = props; + return ( + 📦} color="text-blue-600" inputs={0}> + {data.filePath && ( +
{data.filePath.split('/').pop()}
+ )} + {data.fileType && ( +
{data.fileType.toUpperCase()}
+ )} +
+ ); +} +export const ModelNode = memo(ModelNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/ObjectiveNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/ObjectiveNode.tsx new file mode 100644 index 00000000..5b5b76e8 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/ObjectiveNode.tsx @@ -0,0 +1,20 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { ObjectiveNodeData } from '../../../lib/canvas/schema'; + +function ObjectiveNodeComponent(props: NodeProps) { + const { data } = props; + return ( + 🎯} color="text-red-600"> + {data.name &&
{data.name}
} + {data.direction && ( +
+ {data.direction === 'minimize' ? '↓ Minimize' : '↑ Maximize'} + {data.weight !== 1 && ` (w=${data.weight})`} +
+ )} +
+ ); +} +export const ObjectiveNode = memo(ObjectiveNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/SolverNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/SolverNode.tsx new file mode 100644 index 00000000..18852804 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/SolverNode.tsx @@ -0,0 +1,14 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { SolverNodeData } from '../../../lib/canvas/schema'; + +function SolverNodeComponent(props: NodeProps) { + const { data } = props; + return ( + ⚙️} color="text-purple-600"> + {data.solverType &&
{data.solverType}
} +
+ ); +} +export const SolverNode = memo(SolverNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/SurrogateNode.tsx b/atomizer-dashboard/frontend/src/components/canvas/nodes/SurrogateNode.tsx new file mode 100644 index 00000000..2aa5218e --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/SurrogateNode.tsx @@ -0,0 +1,17 @@ +import { memo } from 'react'; +import { NodeProps } from 'reactflow'; +import { BaseNode } from './BaseNode'; +import { SurrogateNodeData } from '../../../lib/canvas/schema'; + +function SurrogateNodeComponent(props: NodeProps) { + const { data } = props; + return ( + 🚀} color="text-pink-600" outputs={0}> +
{data.enabled ? 'Enabled' : 'Disabled'}
+ {data.enabled && data.modelType && ( +
{data.modelType}
+ )} +
+ ); +} +export const SurrogateNode = memo(SurrogateNodeComponent); diff --git a/atomizer-dashboard/frontend/src/components/canvas/nodes/index.ts b/atomizer-dashboard/frontend/src/components/canvas/nodes/index.ts new file mode 100644 index 00000000..b7520cd2 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/nodes/index.ts @@ -0,0 +1,30 @@ +import { ModelNode } from './ModelNode'; +import { SolverNode } from './SolverNode'; +import { DesignVarNode } from './DesignVarNode'; +import { ExtractorNode } from './ExtractorNode'; +import { ObjectiveNode } from './ObjectiveNode'; +import { ConstraintNode } from './ConstraintNode'; +import { AlgorithmNode } from './AlgorithmNode'; +import { SurrogateNode } from './SurrogateNode'; + +export { + ModelNode, + SolverNode, + DesignVarNode, + ExtractorNode, + ObjectiveNode, + ConstraintNode, + AlgorithmNode, + SurrogateNode, +}; + +export const nodeTypes = { + model: ModelNode, + solver: SolverNode, + designVar: DesignVarNode, + extractor: ExtractorNode, + objective: ObjectiveNode, + constraint: ConstraintNode, + algorithm: AlgorithmNode, + surrogate: SurrogateNode, +}; diff --git a/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx b/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx new file mode 100644 index 00000000..14236761 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/palette/NodePalette.tsx @@ -0,0 +1,52 @@ +import { DragEvent } from 'react'; +import { NodeType } from '../../../lib/canvas/schema'; + +interface PaletteItem { + type: NodeType; + label: string; + icon: string; + description: string; +} + +const PALETTE_ITEMS: PaletteItem[] = [ + { type: 'model', label: 'Model', icon: '📦', description: 'NX model file' }, + { type: 'solver', label: 'Solver', icon: '⚙️', description: 'Nastran solution' }, + { type: 'designVar', label: 'Design Variable', icon: '📐', description: 'Parameter to vary' }, + { type: 'extractor', label: 'Extractor', icon: '🔬', description: 'Physics extraction' }, + { type: 'objective', label: 'Objective', icon: '🎯', description: 'Optimization goal' }, + { type: 'constraint', label: 'Constraint', icon: '🚧', description: 'Limit condition' }, + { type: 'algorithm', label: 'Algorithm', icon: '🧠', description: 'Optimization method' }, + { type: 'surrogate', label: 'Surrogate', icon: '🚀', description: 'Neural acceleration' }, +]; + +export function NodePalette() { + const onDragStart = (event: DragEvent, nodeType: NodeType) => { + event.dataTransfer.setData('application/reactflow', nodeType); + event.dataTransfer.effectAllowed = 'move'; + }; + + return ( +
+

+ Components +

+
+ {PALETTE_ITEMS.map((item) => ( +
onDragStart(e, item.type)} + className="flex items-center gap-3 p-3 bg-white rounded-lg border border-gray-200 + cursor-grab hover:border-blue-300 hover:shadow-sm transition-all" + > + {item.icon} +
+
{item.label}
+
{item.description}
+
+
+ ))} +
+
+ ); +} diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/NodeConfigPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/NodeConfigPanel.tsx new file mode 100644 index 00000000..6cc2c9a7 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/NodeConfigPanel.tsx @@ -0,0 +1,372 @@ +import { useCanvasStore } from '../../../hooks/useCanvasStore'; +import { + ModelNodeData, + SolverNodeData, + DesignVarNodeData, + AlgorithmNodeData, + ObjectiveNodeData, + ExtractorNodeData, + ConstraintNodeData, + SurrogateNodeData +} from '../../../lib/canvas/schema'; + +interface NodeConfigPanelProps { + nodeId: string; +} + +export function NodeConfigPanel({ nodeId }: NodeConfigPanelProps) { + const { nodes, updateNodeData, deleteSelected } = useCanvasStore(); + const node = nodes.find((n) => n.id === nodeId); + + if (!node) return null; + + const { data } = node; + + const handleChange = (field: string, value: unknown) => { + updateNodeData(nodeId, { [field]: value, configured: true }); + }; + + return ( +
+
+

Configure {data.label}

+ +
+ +
+ {/* Common: Label */} +
+ + handleChange('label', e.target.value)} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ + {/* Type-specific fields */} + {data.type === 'model' && ( + <> +
+ + handleChange('filePath', e.target.value)} + placeholder="path/to/model.prt" + className="w-full px-3 py-2 border rounded-lg font-mono text-sm" + /> +
+
+ + +
+ + )} + + {data.type === 'solver' && ( +
+ + +
+ )} + + {data.type === 'designVar' && ( + <> +
+ + handleChange('expressionName', e.target.value)} + placeholder="thickness" + className="w-full px-3 py-2 border rounded-lg font-mono" + /> +
+
+
+ + handleChange('minValue', parseFloat(e.target.value))} + className="w-full px-3 py-2 border rounded-lg" + /> +
+
+ + handleChange('maxValue', parseFloat(e.target.value))} + className="w-full px-3 py-2 border rounded-lg" + /> +
+
+
+ + handleChange('unit', e.target.value)} + placeholder="mm" + className="w-full px-3 py-2 border rounded-lg" + /> +
+ + )} + + {data.type === 'extractor' && ( + <> +
+ + +
+ + )} + + {data.type === 'algorithm' && ( + <> +
+ + +
+
+ + handleChange('maxTrials', parseInt(e.target.value))} + placeholder="100" + className="w-full px-3 py-2 border rounded-lg" + /> +
+ + )} + + {data.type === 'objective' && ( + <> +
+ + handleChange('name', e.target.value)} + placeholder="mass" + className="w-full px-3 py-2 border rounded-lg" + /> +
+
+ + +
+
+ + handleChange('weight', parseFloat(e.target.value))} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ + )} + + {data.type === 'constraint' && ( + <> +
+ + handleChange('name', e.target.value)} + placeholder="max_stress" + className="w-full px-3 py-2 border rounded-lg" + /> +
+
+
+ + +
+
+ + handleChange('value', parseFloat(e.target.value))} + className="w-full px-3 py-2 border rounded-lg" + /> +
+
+ + )} + + {data.type === 'surrogate' && ( + <> +
+ handleChange('enabled', e.target.checked)} + className="w-4 h-4" + /> + +
+ {(data as SurrogateNodeData).enabled && ( + <> +
+ + +
+
+ + handleChange('minTrials', parseInt(e.target.value))} + className="w-full px-3 py-2 border rounded-lg" + /> +
+ + )} + + )} +
+
+ ); +} diff --git a/atomizer-dashboard/frontend/src/components/canvas/panels/ValidationPanel.tsx b/atomizer-dashboard/frontend/src/components/canvas/panels/ValidationPanel.tsx new file mode 100644 index 00000000..5956a9a2 --- /dev/null +++ b/atomizer-dashboard/frontend/src/components/canvas/panels/ValidationPanel.tsx @@ -0,0 +1,32 @@ +import { ValidationResult } from '../../../lib/canvas/validation'; + +interface ValidationPanelProps { + validation: ValidationResult; +} + +export function ValidationPanel({ validation }: ValidationPanelProps) { + return ( +
+ {validation.errors.length > 0 && ( +
+
Errors
+
    + {validation.errors.map((error, i) => ( +
  • {error}
  • + ))} +
+
+ )} + {validation.warnings.length > 0 && ( +
+
Warnings
+
    + {validation.warnings.map((warning, i) => ( +
  • {warning}
  • + ))} +
+
+ )} +
+ ); +} diff --git a/atomizer-dashboard/frontend/src/components/charts/NivoParallelCoordinates.tsx b/atomizer-dashboard/frontend/src/components/charts/NivoParallelCoordinates.tsx index cec9e12e..c067dad0 100644 --- a/atomizer-dashboard/frontend/src/components/charts/NivoParallelCoordinates.tsx +++ b/atomizer-dashboard/frontend/src/components/charts/NivoParallelCoordinates.tsx @@ -91,7 +91,7 @@ function getAvailableParams(trials: Trial[]): string[] { export function NivoParallelCoordinates({ trials, - objectives, + objectives: _objectives, designVariables, paretoFront = [], height = 400 diff --git a/atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts b/atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts new file mode 100644 index 00000000..0ac93d68 --- /dev/null +++ b/atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts @@ -0,0 +1,125 @@ +import { create } from 'zustand'; +import { Node, Edge, addEdge, applyNodeChanges, applyEdgeChanges, Connection, NodeChange, EdgeChange } from 'reactflow'; +import { CanvasNodeData, NodeType } from '../lib/canvas/schema'; +import { validateGraph, ValidationResult } from '../lib/canvas/validation'; +import { serializeToIntent, OptimizationIntent } from '../lib/canvas/intent'; + +interface CanvasState { + nodes: Node[]; + edges: Edge[]; + selectedNode: string | null; + validation: ValidationResult; + + // Actions + onNodesChange: (changes: NodeChange[]) => void; + onEdgesChange: (changes: EdgeChange[]) => void; + onConnect: (connection: Connection) => void; + addNode: (type: NodeType, position: { x: number; y: number }) => void; + updateNodeData: (nodeId: string, data: Partial) => void; + selectNode: (nodeId: string | null) => void; + deleteSelected: () => void; + validate: () => ValidationResult; + toIntent: () => OptimizationIntent; + clear: () => void; + loadFromIntent: (intent: OptimizationIntent) => void; +} + +let nodeIdCounter = 0; +const getNodeId = () => `node_${++nodeIdCounter}`; + +const getDefaultData = (type: NodeType): CanvasNodeData => { + const base = { label: type.charAt(0).toUpperCase() + type.slice(1), configured: false }; + + switch (type) { + case 'model': return { ...base, type: 'model' }; + case 'solver': return { ...base, type: 'solver' }; + case 'designVar': return { ...base, type: 'designVar', label: 'Design Variable' }; + case 'extractor': return { ...base, type: 'extractor' }; + case 'objective': return { ...base, type: 'objective' }; + case 'constraint': return { ...base, type: 'constraint' }; + case 'algorithm': return { ...base, type: 'algorithm' }; + case 'surrogate': return { ...base, type: 'surrogate', enabled: false }; + default: return { ...base, type } as CanvasNodeData; + } +}; + +export const useCanvasStore = create((set, get) => ({ + nodes: [], + edges: [], + selectedNode: null, + validation: { valid: false, errors: [], warnings: [] }, + + onNodesChange: (changes) => { + set({ nodes: applyNodeChanges(changes, get().nodes) }); + }, + + onEdgesChange: (changes) => { + set({ edges: applyEdgeChanges(changes, get().edges) }); + }, + + onConnect: (connection) => { + set({ edges: addEdge(connection, get().edges) }); + }, + + addNode: (type, position) => { + const newNode: Node = { + id: getNodeId(), + type, + position, + data: getDefaultData(type), + }; + set({ nodes: [...get().nodes, newNode] }); + }, + + updateNodeData: (nodeId, data) => { + set({ + nodes: get().nodes.map((node) => + node.id === nodeId + ? { ...node, data: { ...node.data, ...data } } + : node + ), + }); + }, + + selectNode: (nodeId) => { + set({ selectedNode: nodeId }); + }, + + deleteSelected: () => { + const { selectedNode, nodes, edges } = get(); + if (!selectedNode) return; + + set({ + nodes: nodes.filter((n) => n.id !== selectedNode), + edges: edges.filter((e) => e.source !== selectedNode && e.target !== selectedNode), + selectedNode: null, + }); + }, + + validate: () => { + const { nodes, edges } = get(); + const result = validateGraph(nodes, edges); + set({ validation: result }); + return result; + }, + + toIntent: () => { + const { nodes, edges } = get(); + return serializeToIntent(nodes, edges); + }, + + clear: () => { + set({ + nodes: [], + edges: [], + selectedNode: null, + validation: { valid: false, errors: [], warnings: [] }, + }); + nodeIdCounter = 0; + }, + + loadFromIntent: (intent) => { + // TODO: Implement reverse serialization + console.log('Loading intent:', intent); + }, +})); diff --git a/atomizer-dashboard/frontend/src/lib/canvas/intent.ts b/atomizer-dashboard/frontend/src/lib/canvas/intent.ts new file mode 100644 index 00000000..3a76aa85 --- /dev/null +++ b/atomizer-dashboard/frontend/src/lib/canvas/intent.ts @@ -0,0 +1,173 @@ +/** + * Intent Serializer - Convert canvas graph to Intent JSON for Claude + */ + +import { Node, Edge } from 'reactflow'; +import { CanvasNodeData, ExtractorNodeData } from './schema'; + +export interface OptimizationIntent { + version: '1.0'; + source: 'canvas'; + timestamp: string; + model: { + path?: string; + type?: string; + }; + solver: { + type?: string; + }; + design_variables: Array<{ + name: string; + min: number; + max: number; + unit?: string; + }>; + extractors: Array<{ + id: string; + name: string; + config?: Record; + }>; + objectives: Array<{ + name: string; + direction: 'minimize' | 'maximize'; + weight: number; + extractor: string; + }>; + constraints: Array<{ + name: string; + operator: string; + value: number; + extractor: string; + }>; + optimization: { + method?: string; + max_trials?: number; + }; + surrogate?: { + enabled: boolean; + type?: string; + min_trials?: number; + }; +} + +export function serializeToIntent( + nodes: Node[], + edges: Edge[] +): OptimizationIntent { + const intent: OptimizationIntent = { + version: '1.0', + source: 'canvas', + timestamp: new Date().toISOString(), + model: {}, + solver: {}, + design_variables: [], + extractors: [], + objectives: [], + constraints: [], + optimization: {}, + }; + + // Helper to find connected nodes + const getConnectedNodes = (nodeId: string, direction: 'source' | 'target') => { + return edges + .filter(e => direction === 'source' ? e.source === nodeId : e.target === nodeId) + .map(e => direction === 'source' ? e.target : e.source) + .map(id => nodes.find(n => n.id === id)) + .filter(Boolean) as Node[]; + }; + + // Process each node type + for (const node of nodes) { + const data = node.data; + + switch (data.type) { + case 'model': + intent.model = { + path: data.filePath, + type: data.fileType, + }; + break; + + case 'solver': + intent.solver = { + type: data.solverType, + }; + break; + + case 'designVar': + if (data.expressionName) { + intent.design_variables.push({ + name: data.expressionName, + min: data.minValue ?? 0, + max: data.maxValue ?? 100, + unit: data.unit, + }); + } + break; + + case 'extractor': + if (data.extractorId) { + intent.extractors.push({ + id: data.extractorId, + name: data.extractorName ?? data.extractorId, + config: data.config, + }); + } + break; + + case 'objective': + if (data.name) { + // Find connected extractor + const sourceNodes = getConnectedNodes(node.id, 'target'); + const extractor = sourceNodes.find(n => n.data.type === 'extractor'); + + intent.objectives.push({ + name: data.name, + direction: data.direction ?? 'minimize', + weight: data.weight ?? 1, + extractor: extractor?.data.type === 'extractor' + ? (extractor.data as ExtractorNodeData).extractorId ?? '' + : '', + }); + } + break; + + case 'constraint': + if (data.name) { + const sourceNodes = getConnectedNodes(node.id, 'target'); + const extractor = sourceNodes.find(n => n.data.type === 'extractor'); + + intent.constraints.push({ + name: data.name, + operator: data.operator ?? '<=', + value: data.value ?? 0, + extractor: extractor?.data.type === 'extractor' + ? (extractor.data as ExtractorNodeData).extractorId ?? '' + : '', + }); + } + break; + + case 'algorithm': + intent.optimization = { + method: data.method, + max_trials: data.maxTrials, + }; + break; + + case 'surrogate': + intent.surrogate = { + enabled: data.enabled ?? false, + type: data.modelType, + min_trials: data.minTrials, + }; + break; + } + } + + return intent; +} + +export function formatIntentForChat(intent: OptimizationIntent): string { + return `INTENT:${JSON.stringify(intent)}`; +} diff --git a/atomizer-dashboard/frontend/src/lib/canvas/schema.ts b/atomizer-dashboard/frontend/src/lib/canvas/schema.ts new file mode 100644 index 00000000..b5c0d839 --- /dev/null +++ b/atomizer-dashboard/frontend/src/lib/canvas/schema.ts @@ -0,0 +1,102 @@ +/** + * Canvas Schema - Type definitions for optimization workflow nodes + */ + +export type NodeType = + | 'model' + | 'solver' + | 'designVar' + | 'extractor' + | 'objective' + | 'constraint' + | 'algorithm' + | 'surrogate'; + +export interface BaseNodeData { + label: string; + configured: boolean; + errors?: string[]; +} + +export interface ModelNodeData extends BaseNodeData { + type: 'model'; + filePath?: string; + fileType?: 'prt' | 'fem' | 'sim'; +} + +export interface SolverNodeData extends BaseNodeData { + type: 'solver'; + solverType?: 'SOL101' | 'SOL103' | 'SOL105' | 'SOL106' | 'SOL111' | 'SOL112'; +} + +export interface DesignVarNodeData extends BaseNodeData { + type: 'designVar'; + expressionName?: string; + minValue?: number; + maxValue?: number; + unit?: string; +} + +export interface ExtractorNodeData extends BaseNodeData { + type: 'extractor'; + extractorId?: string; + extractorName?: string; + config?: Record; +} + +export interface ObjectiveNodeData extends BaseNodeData { + type: 'objective'; + name?: string; + direction?: 'minimize' | 'maximize'; + weight?: number; +} + +export interface ConstraintNodeData extends BaseNodeData { + type: 'constraint'; + name?: string; + operator?: '<' | '<=' | '>' | '>=' | '=='; + value?: number; +} + +export interface AlgorithmNodeData extends BaseNodeData { + type: 'algorithm'; + method?: 'TPE' | 'CMA-ES' | 'NSGA-II' | 'GP-BO' | 'RandomSearch'; + maxTrials?: number; +} + +export interface SurrogateNodeData extends BaseNodeData { + type: 'surrogate'; + enabled?: boolean; + modelType?: 'MLP' | 'GNN' | 'Ensemble'; + minTrials?: number; +} + +export type CanvasNodeData = + | ModelNodeData + | SolverNodeData + | DesignVarNodeData + | ExtractorNodeData + | ObjectiveNodeData + | ConstraintNodeData + | AlgorithmNodeData + | SurrogateNodeData; + +export interface CanvasEdge { + id: string; + source: string; + target: string; + sourceHandle?: string; + targetHandle?: string; +} + +// Valid connections +export const VALID_CONNECTIONS: Record = { + model: ['solver', 'designVar'], + solver: ['extractor'], + designVar: ['model'], + extractor: ['objective', 'constraint'], + objective: ['algorithm'], + constraint: ['algorithm'], + algorithm: ['surrogate'], + surrogate: [], +}; diff --git a/atomizer-dashboard/frontend/src/lib/canvas/validation.ts b/atomizer-dashboard/frontend/src/lib/canvas/validation.ts new file mode 100644 index 00000000..88823352 --- /dev/null +++ b/atomizer-dashboard/frontend/src/lib/canvas/validation.ts @@ -0,0 +1,91 @@ +/** + * Canvas Validation - Validate optimization workflow graphs + */ + +import { Node, Edge } from 'reactflow'; +import { CanvasNodeData, NodeType, VALID_CONNECTIONS } from './schema'; + +export interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} + +export function validateGraph( + nodes: Node[], + edges: Edge[] +): ValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + + // Check required nodes exist + const nodeTypes = new Set(nodes.map(n => n.data.type)); + + if (!nodeTypes.has('model')) { + errors.push('Missing Model node - add an NX model file'); + } + if (!nodeTypes.has('solver')) { + errors.push('Missing Solver node - specify solution type'); + } + if (!nodeTypes.has('objective')) { + errors.push('Missing Objective node - define what to optimize'); + } + if (!nodeTypes.has('algorithm')) { + errors.push('Missing Algorithm node - select optimization method'); + } + + // Check design variables + const designVars = nodes.filter(n => n.data.type === 'designVar'); + if (designVars.length === 0) { + errors.push('No design variables - add at least one parameter to vary'); + } + + // Check extractors + const extractors = nodes.filter(n => n.data.type === 'extractor'); + if (extractors.length === 0) { + errors.push('No extractors - add physics extractors for objectives'); + } + + // Check node configurations + for (const node of nodes) { + if (!node.data.configured) { + warnings.push(`${node.data.label} is not fully configured`); + } + if (node.data.errors?.length) { + errors.push(...node.data.errors.map(e => `${node.data.label}: ${e}`)); + } + } + + // Validate edge connections + for (const edge of edges) { + const source = nodes.find(n => n.id === edge.source); + const target = nodes.find(n => n.id === edge.target); + + if (source && target) { + const sourceType = source.data.type as NodeType; + const targetType = target.data.type as NodeType; + const validTargets = VALID_CONNECTIONS[sourceType] || []; + + if (!validTargets.includes(targetType)) { + errors.push( + `Invalid connection: ${source.data.label} -> ${target.data.label}` + ); + } + } + } + + // Check connectivity + const objectives = nodes.filter(n => n.data.type === 'objective'); + for (const obj of objectives) { + const hasIncoming = edges.some(e => e.target === obj.id); + if (!hasIncoming) { + errors.push(`${obj.data.label} has no connected extractor`); + } + } + + return { + valid: errors.length === 0, + errors, + warnings, + }; +} diff --git a/atomizer-dashboard/frontend/src/pages/CanvasView.tsx b/atomizer-dashboard/frontend/src/pages/CanvasView.tsx new file mode 100644 index 00000000..400424fa --- /dev/null +++ b/atomizer-dashboard/frontend/src/pages/CanvasView.tsx @@ -0,0 +1,21 @@ +import { AtomizerCanvas } from '../components/canvas/AtomizerCanvas'; + +export function CanvasView() { + return ( +
+
+

+ Optimization Canvas +

+

+ Drag components from the palette to build your optimization workflow +

+
+
+ +
+
+ ); +} + +export default CanvasView;