feat: Phase 1 - Canvas with React Flow
- 8 node types (Model, Solver, DesignVar, Extractor, Objective, Constraint, Algorithm, Surrogate) - Drag-drop from palette to canvas - Node configuration panels - Graph validation with error/warning display - Intent JSON serialization - Zustand state management Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
548
atomizer-dashboard/frontend/package-lock.json
generated
548
atomizer-dashboard/frontend/package-lock.json
generated
@@ -29,13 +29,15 @@
|
|||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.20.0",
|
||||||
"react-syntax-highlighter": "^16.1.0",
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
|
"reactflow": "^11.11.4",
|
||||||
"recharts": "^2.10.3",
|
"recharts": "^2.10.3",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"three": "^0.181.2",
|
"three": "^0.181.2",
|
||||||
"xterm": "^5.3.0"
|
"xterm": "^5.3.0",
|
||||||
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
@@ -1468,6 +1470,276 @@
|
|||||||
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@remix-run/router": {
|
||||||
"version": "1.23.1",
|
"version": "1.23.1",
|
||||||
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz",
|
||||||
@@ -1869,30 +2141,159 @@
|
|||||||
"@babel/types": "^7.28.2"
|
"@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": {
|
"node_modules/@types/d3-array": {
|
||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-color": {
|
||||||
"version": "3.1.3",
|
"version": "3.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-ease": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-format": {
|
||||||
"version": "1.4.5",
|
"version": "1.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-1.4.5.tgz",
|
||||||
"integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==",
|
"integrity": "sha512-mLxrC1MSWupOSncXN/HOlWUAAIffAEBaI4+PKy2uMPsKe4FNZlk7qrbTjmzJXITQQqBHivaks4Td18azgqnotA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-interpolate": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
@@ -1908,6 +2309,24 @@
|
|||||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-scale": {
|
||||||
"version": "4.0.9",
|
"version": "4.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
@@ -1923,6 +2342,12 @@
|
|||||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/d3-shape": {
|
||||||
"version": "3.1.7",
|
"version": "3.1.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
@@ -1950,6 +2375,25 @@
|
|||||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -1980,6 +2424,12 @@
|
|||||||
"@types/estree": "*"
|
"@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": {
|
"node_modules/@types/hast": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||||
@@ -2854,6 +3304,12 @@
|
|||||||
"node": ">= 6"
|
"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": {
|
"node_modules/clsx": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||||
@@ -2989,6 +3445,28 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/d3-ease": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
@@ -3057,6 +3535,15 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/d3-shape": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
@@ -3102,6 +3589,41 @@
|
|||||||
"node": ">=12"
|
"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": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"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"
|
"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": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@@ -7549,9 +8089,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/zustand": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz",
|
||||||
"integrity": "sha512-gyPKpIaxY9XcO2vSMrLbiER7QMAMGOQZVRdJ6Zi782jkbzZygq5GI9nG8g+sMgitRtndwaBSl7uiqC49o1SSiw==",
|
"integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12.20.0"
|
"node": ">=12.20.0"
|
||||||
|
|||||||
@@ -31,13 +31,15 @@
|
|||||||
"react-router-dom": "^6.20.0",
|
"react-router-dom": "^6.20.0",
|
||||||
"react-syntax-highlighter": "^16.1.0",
|
"react-syntax-highlighter": "^16.1.0",
|
||||||
"react-use-websocket": "^4.13.0",
|
"react-use-websocket": "^4.13.0",
|
||||||
|
"reactflow": "^11.11.4",
|
||||||
"recharts": "^2.10.3",
|
"recharts": "^2.10.3",
|
||||||
"rehype-katex": "^7.0.1",
|
"rehype-katex": "^7.0.1",
|
||||||
"remark-gfm": "^4.0.1",
|
"remark-gfm": "^4.0.1",
|
||||||
"remark-math": "^6.0.0",
|
"remark-math": "^6.0.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"three": "^0.181.2",
|
"three": "^0.181.2",
|
||||||
"xterm": "^5.3.0"
|
"xterm": "^5.3.0",
|
||||||
|
"zustand": "^5.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.43",
|
"@types/react": "^18.2.43",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import Dashboard from './pages/Dashboard';
|
|||||||
import Analysis from './pages/Analysis';
|
import Analysis from './pages/Analysis';
|
||||||
import Insights from './pages/Insights';
|
import Insights from './pages/Insights';
|
||||||
import Results from './pages/Results';
|
import Results from './pages/Results';
|
||||||
|
import CanvasView from './pages/CanvasView';
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@@ -27,6 +28,9 @@ function App() {
|
|||||||
{/* Home page - no sidebar layout */}
|
{/* Home page - no sidebar layout */}
|
||||||
<Route path="/" element={<Home />} />
|
<Route path="/" element={<Home />} />
|
||||||
|
|
||||||
|
{/* Canvas page - full screen, no sidebar */}
|
||||||
|
<Route path="canvas" element={<CanvasView />} />
|
||||||
|
|
||||||
{/* Study pages - with sidebar layout */}
|
{/* Study pages - with sidebar layout */}
|
||||||
<Route element={<MainLayout />}>
|
<Route element={<MainLayout />}>
|
||||||
<Route path="setup" element={<Setup />} />
|
<Route path="setup" element={<Setup />} />
|
||||||
|
|||||||
@@ -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<HTMLDivElement>(null);
|
||||||
|
const reactFlowInstance = useRef<ReactFlowInstance | null>(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 (
|
||||||
|
<div className="flex h-full">
|
||||||
|
{/* Left: Node Palette */}
|
||||||
|
<NodePalette />
|
||||||
|
|
||||||
|
{/* Center: Canvas */}
|
||||||
|
<div className="flex-1 relative" ref={reactFlowWrapper}>
|
||||||
|
<ReactFlow
|
||||||
|
nodes={nodes}
|
||||||
|
edges={edges}
|
||||||
|
onNodesChange={onNodesChange}
|
||||||
|
onEdgesChange={onEdgesChange}
|
||||||
|
onConnect={onConnect}
|
||||||
|
onInit={(instance) => { reactFlowInstance.current = instance; }}
|
||||||
|
onDragOver={onDragOver}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onNodeClick={onNodeClick}
|
||||||
|
onPaneClick={onPaneClick}
|
||||||
|
nodeTypes={nodeTypes}
|
||||||
|
fitView
|
||||||
|
>
|
||||||
|
<Background />
|
||||||
|
<Controls />
|
||||||
|
<MiniMap />
|
||||||
|
</ReactFlow>
|
||||||
|
|
||||||
|
{/* Execute Button */}
|
||||||
|
<div className="absolute bottom-4 right-4 flex gap-2 z-10">
|
||||||
|
<button
|
||||||
|
onClick={validate}
|
||||||
|
className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition-colors"
|
||||||
|
>
|
||||||
|
Validate
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleExecute}
|
||||||
|
disabled={!validation.valid}
|
||||||
|
className={`px-4 py-2 rounded-lg transition-colors ${
|
||||||
|
validation.valid
|
||||||
|
? 'bg-blue-600 text-white hover:bg-blue-700'
|
||||||
|
: 'bg-gray-300 text-gray-500 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Execute with Claude
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Validation Messages */}
|
||||||
|
{(validation.errors.length > 0 || validation.warnings.length > 0) && (
|
||||||
|
<ValidationPanel validation={validation} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right: Config Panel */}
|
||||||
|
{selectedNode && <NodeConfigPanel nodeId={selectedNode} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AtomizerCanvas() {
|
||||||
|
return (
|
||||||
|
<ReactFlowProvider>
|
||||||
|
<CanvasFlow />
|
||||||
|
</ReactFlowProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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<AlgorithmNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>🧠</span>} color="text-indigo-600">
|
||||||
|
{data.method && <div>{data.method}</div>}
|
||||||
|
{data.maxTrials && (
|
||||||
|
<div className="text-xs text-gray-400">{data.maxTrials} trials</div>
|
||||||
|
)}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const AlgorithmNode = memo(AlgorithmNodeComponent);
|
||||||
@@ -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<BaseNodeData> {
|
||||||
|
icon: ReactNode;
|
||||||
|
color: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
inputs?: number;
|
||||||
|
outputs?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BaseNodeComponent({
|
||||||
|
data,
|
||||||
|
selected,
|
||||||
|
icon,
|
||||||
|
color,
|
||||||
|
children,
|
||||||
|
inputs = 1,
|
||||||
|
outputs = 1,
|
||||||
|
}: BaseNodeProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
px-4 py-3 rounded-lg border-2 min-w-[180px] bg-white shadow-sm
|
||||||
|
transition-all duration-200
|
||||||
|
${selected ? 'border-blue-500 shadow-lg' : 'border-gray-200'}
|
||||||
|
${!data.configured ? 'border-dashed' : ''}
|
||||||
|
${data.errors?.length ? 'border-red-400' : ''}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{/* Input handles */}
|
||||||
|
{inputs > 0 && (
|
||||||
|
<Handle
|
||||||
|
type="target"
|
||||||
|
position={Position.Left}
|
||||||
|
className="w-3 h-3 !bg-gray-400"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Header */}
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<span className={`text-lg ${color}`}>{icon}</span>
|
||||||
|
<span className="font-medium text-gray-800">{data.label}</span>
|
||||||
|
{!data.configured && (
|
||||||
|
<span className="text-xs text-orange-500">!</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
{children && <div className="text-sm text-gray-600">{children}</div>}
|
||||||
|
|
||||||
|
{/* Errors */}
|
||||||
|
{data.errors?.length ? (
|
||||||
|
<div className="mt-2 text-xs text-red-500">
|
||||||
|
{data.errors[0]}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{/* Output handles */}
|
||||||
|
{outputs > 0 && (
|
||||||
|
<Handle
|
||||||
|
type="source"
|
||||||
|
position={Position.Right}
|
||||||
|
className="w-3 h-3 !bg-gray-400"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BaseNode = memo(BaseNodeComponent);
|
||||||
@@ -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<ConstraintNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>🚧</span>} color="text-orange-600">
|
||||||
|
{data.name && <div>{data.name}</div>}
|
||||||
|
{data.operator && data.value !== undefined && (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{data.operator} {data.value}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const ConstraintNode = memo(ConstraintNodeComponent);
|
||||||
@@ -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<DesignVarNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>📐</span>} color="text-green-600">
|
||||||
|
{data.expressionName && <div className="font-mono">{data.expressionName}</div>}
|
||||||
|
{data.minValue !== undefined && data.maxValue !== undefined && (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
[{data.minValue} - {data.maxValue}] {data.unit || ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const DesignVarNode = memo(DesignVarNodeComponent);
|
||||||
@@ -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<ExtractorNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>🔬</span>} color="text-cyan-600">
|
||||||
|
{data.extractorName && <div>{data.extractorName}</div>}
|
||||||
|
{data.extractorId && (
|
||||||
|
<div className="text-xs text-gray-400">{data.extractorId}</div>
|
||||||
|
)}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const ExtractorNode = memo(ExtractorNodeComponent);
|
||||||
@@ -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<ModelNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>📦</span>} color="text-blue-600" inputs={0}>
|
||||||
|
{data.filePath && (
|
||||||
|
<div className="truncate max-w-[150px]">{data.filePath.split('/').pop()}</div>
|
||||||
|
)}
|
||||||
|
{data.fileType && (
|
||||||
|
<div className="text-xs text-gray-400">{data.fileType.toUpperCase()}</div>
|
||||||
|
)}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const ModelNode = memo(ModelNodeComponent);
|
||||||
@@ -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<ObjectiveNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>🎯</span>} color="text-red-600">
|
||||||
|
{data.name && <div>{data.name}</div>}
|
||||||
|
{data.direction && (
|
||||||
|
<div className="text-xs text-gray-400">
|
||||||
|
{data.direction === 'minimize' ? '↓ Minimize' : '↑ Maximize'}
|
||||||
|
{data.weight !== 1 && ` (w=${data.weight})`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const ObjectiveNode = memo(ObjectiveNodeComponent);
|
||||||
@@ -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<SolverNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>⚙️</span>} color="text-purple-600">
|
||||||
|
{data.solverType && <div>{data.solverType}</div>}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const SolverNode = memo(SolverNodeComponent);
|
||||||
@@ -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<SurrogateNodeData>) {
|
||||||
|
const { data } = props;
|
||||||
|
return (
|
||||||
|
<BaseNode {...props} icon={<span>🚀</span>} color="text-pink-600" outputs={0}>
|
||||||
|
<div>{data.enabled ? 'Enabled' : 'Disabled'}</div>
|
||||||
|
{data.enabled && data.modelType && (
|
||||||
|
<div className="text-xs text-gray-400">{data.modelType}</div>
|
||||||
|
)}
|
||||||
|
</BaseNode>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export const SurrogateNode = memo(SurrogateNodeComponent);
|
||||||
@@ -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,
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<div className="w-64 bg-gray-50 border-r border-gray-200 p-4 overflow-y-auto">
|
||||||
|
<h3 className="text-sm font-semibold text-gray-500 uppercase mb-4">
|
||||||
|
Components
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{PALETTE_ITEMS.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.type}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => 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"
|
||||||
|
>
|
||||||
|
<span className="text-xl">{item.icon}</span>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium text-gray-800">{item.label}</div>
|
||||||
|
<div className="text-xs text-gray-500">{item.description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 (
|
||||||
|
<div className="w-80 bg-white border-l border-gray-200 p-4 overflow-y-auto">
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<h3 className="font-semibold text-gray-800">Configure {data.label}</h3>
|
||||||
|
<button
|
||||||
|
onClick={deleteSelected}
|
||||||
|
className="text-red-500 hover:text-red-700 text-sm"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Common: Label */}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Label
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={data.label}
|
||||||
|
onChange={(e) => handleChange('label', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Type-specific fields */}
|
||||||
|
{data.type === 'model' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
File Path
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={(data as ModelNodeData).filePath || ''}
|
||||||
|
onChange={(e) => handleChange('filePath', e.target.value)}
|
||||||
|
placeholder="path/to/model.prt"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
File Type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={(data as ModelNodeData).fileType || ''}
|
||||||
|
onChange={(e) => handleChange('fileType', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<option value="prt">Part (.prt)</option>
|
||||||
|
<option value="fem">FEM (.fem)</option>
|
||||||
|
<option value="sim">Simulation (.sim)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.type === 'solver' && (
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Solution Type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={(data as SolverNodeData).solverType || ''}
|
||||||
|
onChange={(e) => handleChange('solverType', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<option value="SOL101">SOL 101 - Linear Static</option>
|
||||||
|
<option value="SOL103">SOL 103 - Modal Analysis</option>
|
||||||
|
<option value="SOL105">SOL 105 - Buckling</option>
|
||||||
|
<option value="SOL106">SOL 106 - Nonlinear Static</option>
|
||||||
|
<option value="SOL111">SOL 111 - Frequency Response</option>
|
||||||
|
<option value="SOL112">SOL 112 - Transient Response</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.type === 'designVar' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Expression Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={(data as DesignVarNodeData).expressionName || ''}
|
||||||
|
onChange={(e) => handleChange('expressionName', e.target.value)}
|
||||||
|
placeholder="thickness"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg font-mono"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Min
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={(data as DesignVarNodeData).minValue ?? ''}
|
||||||
|
onChange={(e) => handleChange('minValue', parseFloat(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Max
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={(data as DesignVarNodeData).maxValue ?? ''}
|
||||||
|
onChange={(e) => handleChange('maxValue', parseFloat(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Unit
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={(data as DesignVarNodeData).unit || ''}
|
||||||
|
onChange={(e) => handleChange('unit', e.target.value)}
|
||||||
|
placeholder="mm"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.type === 'extractor' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Extractor ID
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={(data as ExtractorNodeData).extractorId || ''}
|
||||||
|
onChange={(e) => {
|
||||||
|
const id = e.target.value;
|
||||||
|
const names: Record<string, string> = {
|
||||||
|
'E1': 'Displacement',
|
||||||
|
'E2': 'Frequency',
|
||||||
|
'E3': 'Solid Stress',
|
||||||
|
'E4': 'BDF Mass',
|
||||||
|
'E5': 'CAD Mass',
|
||||||
|
'E8': 'Zernike (OP2)',
|
||||||
|
'E9': 'Zernike (CSV)',
|
||||||
|
'E10': 'Zernike (RMS)',
|
||||||
|
};
|
||||||
|
handleChange('extractorId', id);
|
||||||
|
handleChange('extractorName', names[id] || id);
|
||||||
|
}}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<option value="E1">E1 - Displacement</option>
|
||||||
|
<option value="E2">E2 - Frequency</option>
|
||||||
|
<option value="E3">E3 - Solid Stress</option>
|
||||||
|
<option value="E4">E4 - BDF Mass</option>
|
||||||
|
<option value="E5">E5 - CAD Mass</option>
|
||||||
|
<option value="E8">E8 - Zernike (OP2)</option>
|
||||||
|
<option value="E9">E9 - Zernike (CSV)</option>
|
||||||
|
<option value="E10">E10 - Zernike (RMS)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.type === 'algorithm' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Method
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={(data as AlgorithmNodeData).method || ''}
|
||||||
|
onChange={(e) => handleChange('method', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<option value="TPE">TPE (Tree Parzen Estimator)</option>
|
||||||
|
<option value="CMA-ES">CMA-ES (Evolution Strategy)</option>
|
||||||
|
<option value="NSGA-II">NSGA-II (Multi-Objective)</option>
|
||||||
|
<option value="GP-BO">GP-BO (Gaussian Process)</option>
|
||||||
|
<option value="RandomSearch">Random Search</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Max Trials
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={(data as AlgorithmNodeData).maxTrials ?? ''}
|
||||||
|
onChange={(e) => handleChange('maxTrials', parseInt(e.target.value))}
|
||||||
|
placeholder="100"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.type === 'objective' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Objective Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={(data as ObjectiveNodeData).name || ''}
|
||||||
|
onChange={(e) => handleChange('name', e.target.value)}
|
||||||
|
placeholder="mass"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Direction
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={(data as ObjectiveNodeData).direction || 'minimize'}
|
||||||
|
onChange={(e) => handleChange('direction', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="minimize">Minimize</option>
|
||||||
|
<option value="maximize">Maximize</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Weight
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="0.1"
|
||||||
|
value={(data as ObjectiveNodeData).weight ?? 1}
|
||||||
|
onChange={(e) => handleChange('weight', parseFloat(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.type === 'constraint' && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Constraint Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={(data as ConstraintNodeData).name || ''}
|
||||||
|
onChange={(e) => handleChange('name', e.target.value)}
|
||||||
|
placeholder="max_stress"
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Operator
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={(data as ConstraintNodeData).operator || '<='}
|
||||||
|
onChange={(e) => handleChange('operator', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="<"><</option>
|
||||||
|
<option value="<="><=</option>
|
||||||
|
<option value=">">></option>
|
||||||
|
<option value=">=">>=</option>
|
||||||
|
<option value="==">==</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Value
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={(data as ConstraintNodeData).value ?? ''}
|
||||||
|
onChange={(e) => handleChange('value', parseFloat(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data.type === 'surrogate' && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="surrogate-enabled"
|
||||||
|
checked={(data as SurrogateNodeData).enabled || false}
|
||||||
|
onChange={(e) => handleChange('enabled', e.target.checked)}
|
||||||
|
className="w-4 h-4"
|
||||||
|
/>
|
||||||
|
<label htmlFor="surrogate-enabled" className="text-sm font-medium text-gray-600">
|
||||||
|
Enable Neural Surrogate
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{(data as SurrogateNodeData).enabled && (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Model Type
|
||||||
|
</label>
|
||||||
|
<select
|
||||||
|
value={(data as SurrogateNodeData).modelType || ''}
|
||||||
|
onChange={(e) => handleChange('modelType', e.target.value)}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
>
|
||||||
|
<option value="">Select...</option>
|
||||||
|
<option value="MLP">MLP (Multi-Layer Perceptron)</option>
|
||||||
|
<option value="GNN">GNN (Graph Neural Network)</option>
|
||||||
|
<option value="Ensemble">Ensemble</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-600 mb-1">
|
||||||
|
Min Trials Before Activation
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
value={(data as SurrogateNodeData).minTrials ?? 20}
|
||||||
|
onChange={(e) => handleChange('minTrials', parseInt(e.target.value))}
|
||||||
|
className="w-full px-3 py-2 border rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { ValidationResult } from '../../../lib/canvas/validation';
|
||||||
|
|
||||||
|
interface ValidationPanelProps {
|
||||||
|
validation: ValidationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ValidationPanel({ validation }: ValidationPanelProps) {
|
||||||
|
return (
|
||||||
|
<div className="absolute top-4 left-1/2 transform -translate-x-1/2 max-w-md w-full z-10">
|
||||||
|
{validation.errors.length > 0 && (
|
||||||
|
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mb-2">
|
||||||
|
<div className="font-medium text-red-800 mb-1">Errors</div>
|
||||||
|
<ul className="text-sm text-red-600 list-disc list-inside">
|
||||||
|
{validation.errors.map((error, i) => (
|
||||||
|
<li key={i}>{error}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{validation.warnings.length > 0 && (
|
||||||
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||||
|
<div className="font-medium text-yellow-800 mb-1">Warnings</div>
|
||||||
|
<ul className="text-sm text-yellow-600 list-disc list-inside">
|
||||||
|
{validation.warnings.map((warning, i) => (
|
||||||
|
<li key={i}>{warning}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -91,7 +91,7 @@ function getAvailableParams(trials: Trial[]): string[] {
|
|||||||
|
|
||||||
export function NivoParallelCoordinates({
|
export function NivoParallelCoordinates({
|
||||||
trials,
|
trials,
|
||||||
objectives,
|
objectives: _objectives,
|
||||||
designVariables,
|
designVariables,
|
||||||
paretoFront = [],
|
paretoFront = [],
|
||||||
height = 400
|
height = 400
|
||||||
|
|||||||
125
atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts
Normal file
125
atomizer-dashboard/frontend/src/hooks/useCanvasStore.ts
Normal file
@@ -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<CanvasNodeData>[];
|
||||||
|
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<CanvasNodeData>) => 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<CanvasState>((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<CanvasNodeData> = {
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
}));
|
||||||
173
atomizer-dashboard/frontend/src/lib/canvas/intent.ts
Normal file
173
atomizer-dashboard/frontend/src/lib/canvas/intent.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
}>;
|
||||||
|
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<CanvasNodeData>[],
|
||||||
|
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<CanvasNodeData>[];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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)}`;
|
||||||
|
}
|
||||||
102
atomizer-dashboard/frontend/src/lib/canvas/schema.ts
Normal file
102
atomizer-dashboard/frontend/src/lib/canvas/schema.ts
Normal file
@@ -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<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<NodeType, NodeType[]> = {
|
||||||
|
model: ['solver', 'designVar'],
|
||||||
|
solver: ['extractor'],
|
||||||
|
designVar: ['model'],
|
||||||
|
extractor: ['objective', 'constraint'],
|
||||||
|
objective: ['algorithm'],
|
||||||
|
constraint: ['algorithm'],
|
||||||
|
algorithm: ['surrogate'],
|
||||||
|
surrogate: [],
|
||||||
|
};
|
||||||
91
atomizer-dashboard/frontend/src/lib/canvas/validation.ts
Normal file
91
atomizer-dashboard/frontend/src/lib/canvas/validation.ts
Normal file
@@ -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<CanvasNodeData>[],
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
21
atomizer-dashboard/frontend/src/pages/CanvasView.tsx
Normal file
21
atomizer-dashboard/frontend/src/pages/CanvasView.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { AtomizerCanvas } from '../components/canvas/AtomizerCanvas';
|
||||||
|
|
||||||
|
export function CanvasView() {
|
||||||
|
return (
|
||||||
|
<div className="h-screen flex flex-col">
|
||||||
|
<header className="bg-white border-b border-gray-200 px-6 py-4">
|
||||||
|
<h1 className="text-xl font-bold text-gray-800">
|
||||||
|
Optimization Canvas
|
||||||
|
</h1>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Drag components from the palette to build your optimization workflow
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
<main className="flex-1 overflow-hidden">
|
||||||
|
<AtomizerCanvas />
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CanvasView;
|
||||||
Reference in New Issue
Block a user