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:
2026-01-14 20:00:35 -05:00
parent 73a7b9d9f1
commit 7919511bb2
24 changed files with 1915 additions and 6 deletions

View File

@@ -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"

View File

@@ -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",

View File

@@ -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 */}
<Route path="/" element={<Home />} />
{/* Canvas page - full screen, no sidebar */}
<Route path="canvas" element={<CanvasView />} />
{/* Study pages - with sidebar layout */}
<Route element={<MainLayout />}>
<Route path="setup" element={<Setup />} />

View File

@@ -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>
);
}

View File

@@ -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';

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,
};

View File

@@ -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>
);
}

View File

@@ -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="<">&lt;</option>
<option value="<=">&lt;=</option>
<option value=">">&gt;</option>
<option value=">=">&gt;=</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>
);
}

View File

@@ -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>
);
}

View File

@@ -91,7 +91,7 @@ function getAvailableParams(trials: Trial[]): string[] {
export function NivoParallelCoordinates({
trials,
objectives,
objectives: _objectives,
designVariables,
paretoFront = [],
height = 400

View 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);
},
}));

View 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)}`;
}

View 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: [],
};

View 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,
};
}

View 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;