diff --git a/package-lock.json b/package-lock.json index 789e697..2ad154f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,10 +29,15 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", + "@reactflow/background": "^11.3.14", + "@reactflow/controls": "^11.2.14", + "@reactflow/minimap": "^11.7.14", + "@reactflow/node-toolbar": "^1.3.14", "@sentry/browser": "^10.34.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^11.15.0", + "immer": "^11.1.3", "lucide-react": "^0.462.0", "monaco-editor": "^0.52.2", "next": "^14.2.35", @@ -42,11 +47,12 @@ "react-dom": "^18.3.1", "react-error-boundary": "^6.1.0", "react-resizable-panels": "^4.4.1", + "reactflow": "^11.11.4", "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", - "zustand": "^5.0.3" + "zustand": "^5.0.10" }, "devDependencies": { "@testing-library/jest-dom": "^6.9.1", @@ -170,7 +176,6 @@ "version": "7.28.6", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/generator": "^7.28.6", @@ -552,7 +557,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -592,7 +596,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1511,7 +1514,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -3080,6 +3082,276 @@ "version": "1.1.1", "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/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "dev": true, @@ -3556,6 +3828,7 @@ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "dequal": "^2.0.3" } @@ -3647,7 +3920,8 @@ "node_modules/@types/aria-query": { "version": "5.0.4", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -3751,6 +4025,259 @@ "assertion-error": "^2.0.1" } }, + "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": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "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", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "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", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "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.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "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/deep-eql": { "version": "4.0.2", "dev": true, @@ -3761,6 +4288,12 @@ "dev": true, "license": "MIT" }, + "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/json5": { "version": "0.0.29", "dev": true, @@ -3782,7 +4315,6 @@ "version": "18.3.27", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -3792,7 +4324,6 @@ "version": "18.3.7", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -3806,7 +4337,6 @@ "version": "8.53.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.53.0", "@typescript-eslint/types": "8.53.0", @@ -4412,7 +4942,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5288,7 +5817,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -5456,6 +5984,12 @@ "url": "https://polar.sh/cva" } }, + "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/client-only": { "version": "0.0.1", "license": "MIT" @@ -5579,6 +6113,111 @@ "devOptional": true, "license": "MIT" }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "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", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "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-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "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/damerau-levenshtein": { "version": "1.0.8", "dev": true, @@ -5705,6 +6344,7 @@ "version": "2.0.3", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -5735,7 +6375,8 @@ "node_modules/dom-accessibility-api": { "version": "0.5.16", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dompurify": { "version": "3.3.1", @@ -6054,7 +6695,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -6289,7 +6929,6 @@ "version": "2.32.0", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -7034,6 +7673,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz", + "integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "dev": true, @@ -7525,7 +8174,6 @@ "node_modules/jiti": { "version": "1.21.7", "license": "MIT", - "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -7551,7 +8199,6 @@ "integrity": "sha512-mjzqwWRD9Y1J1KUi7W97Gja1bwOOM5Ug0EZ6UDK3xS7j7mndrkwozHtSblfomlzyB4NepioNt+B2sOSzczVgtQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@acemir/cssom": "^0.9.28", "@asamuzakjp/dom-selector": "^6.7.6", @@ -7741,6 +8388,7 @@ "version": "1.5.0", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -7825,8 +8473,7 @@ "version": "0.52.2", "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.2.tgz", "integrity": "sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/motion-dom": { "version": "11.18.1", @@ -8438,7 +9085,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -8605,6 +9251,7 @@ "version": "27.5.1", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -8618,6 +9265,7 @@ "version": "5.2.0", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -8695,7 +9343,6 @@ "node_modules/react": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -8706,7 +9353,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -8725,7 +9371,8 @@ "node_modules/react-is": { "version": "17.0.2", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/react-refresh": { "version": "0.18.0", @@ -8806,6 +9453,24 @@ } } }, + "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", "license": "MIT", @@ -9956,7 +10621,6 @@ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", "license": "MIT", - "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -10063,7 +10727,6 @@ "node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10266,7 +10929,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -10420,7 +11082,6 @@ "version": "7.3.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -10510,7 +11171,6 @@ "version": "4.0.3", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10914,6 +11574,8 @@ }, "node_modules/zustand": { "version": "5.0.10", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", + "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/package.json b/package.json index 674f8e0..757d82b 100644 --- a/package.json +++ b/package.json @@ -34,10 +34,15 @@ "@radix-ui/react-tabs": "^1.1.2", "@radix-ui/react-toast": "^1.2.4", "@radix-ui/react-tooltip": "^1.1.6", + "@reactflow/background": "^11.3.14", + "@reactflow/controls": "^11.2.14", + "@reactflow/minimap": "^11.7.14", + "@reactflow/node-toolbar": "^1.3.14", "@sentry/browser": "^10.34.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^11.15.0", + "immer": "^11.1.3", "lucide-react": "^0.462.0", "monaco-editor": "^0.52.2", "next": "^14.2.35", @@ -47,11 +52,12 @@ "react-dom": "^18.3.1", "react-error-boundary": "^6.1.0", "react-resizable-panels": "^4.4.1", + "reactflow": "^11.11.4", "socket.io-client": "^4.8.1", "sonner": "^2.0.7", "tailwind-merge": "^2.6.0", "tailwindcss-animate": "^1.0.7", - "zustand": "^5.0.3" + "zustand": "^5.0.10" }, "devDependencies": { "@testing-library/jest-dom": "^6.9.1", diff --git a/src/App.tsx b/src/App.tsx index 638da45..14dd03b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,6 +10,7 @@ import { FileSearchModal } from './components/FileSearchModal'; import { SearchInFilesPanel } from './components/SearchInFilesPanel'; import { CommandPalette, createDefaultCommands } from './components/CommandPalette'; import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable'; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from './components/ui/dialog'; import { useIsMobile } from './hooks/use-mobile'; import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts'; import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs'; @@ -32,6 +33,7 @@ const EducationPanel = lazy(() => import('./components/EducationPanel').then(m = const PassportLogin = lazy(() => import('./components/PassportLogin').then(m => ({ default: m.PassportLogin }))); const TranslationPanel = lazy(() => import('./components/TranslationPanel').then(m => ({ default: m.TranslationPanel }))); const AvatarToolkit = lazy(() => import('./components/AvatarToolkit')); +const VisualScriptingCanvas = lazy(() => import('./components/visual-scripting/VisualScriptingCanvas')); function App() { const [currentCode, setCurrentCode] = useState(''); @@ -43,6 +45,7 @@ function App() { const [showSearchInFiles, setShowSearchInFiles] = useState(false); const [showTranslation, setShowTranslation] = useState(false); const [showAvatarToolkit, setShowAvatarToolkit] = useState(false); + const [showVisualScripting, setShowVisualScripting] = useState(false); const [code, setCode] = useState(''); const [currentPlatform, setCurrentPlatform] = useState('roblox'); const isMobile = useIsMobile(); @@ -479,6 +482,7 @@ end)`, onPreviewClick={() => setShowPreview(true)} onNewProjectClick={() => setShowNewProject(true)} onAvatarToolkitClick={() => setShowAvatarToolkit(true)} + onVisualScriptingClick={() => setShowVisualScripting(true)} /> @@ -599,6 +603,31 @@ end)`, /> )} + }> + {showVisualScripting && ( + + + + + + + + Visual Scripting + + +
+ { + setCurrentCode(code); + handleCodeChange(code); + }} + /> +
+
+
+ )} +
diff --git a/src/components/Toolbar.tsx b/src/components/Toolbar.tsx index b258bf0..85311ab 100644 --- a/src/components/Toolbar.tsx +++ b/src/components/Toolbar.tsx @@ -7,7 +7,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; -import { Copy, FileCode, Download, Info, Play, FolderPlus, User, SignOut, List, ArrowsLeftRight, UserCircle } from '@phosphor-icons/react'; +import { Copy, FileCode, Download, Info, Play, FolderPlus, User, SignOut, List, ArrowsLeftRight, UserCircle, GitBranch } from '@phosphor-icons/react'; import { toast } from 'sonner'; import { useState, useEffect, useCallback, memo } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'; @@ -25,9 +25,10 @@ interface ToolbarProps { onPlatformChange: (platform: PlatformId) => void; onTranslateClick?: () => void; onAvatarToolkitClick?: () => void; + onVisualScriptingClick?: () => void; } -export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectClick, currentPlatform, onPlatformChange, onTranslateClick, onAvatarToolkitClick }: ToolbarProps) { +export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectClick, currentPlatform, onPlatformChange, onTranslateClick, onAvatarToolkitClick, onVisualScriptingClick }: ToolbarProps) { const [showInfo, setShowInfo] = useState(false); const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(null); @@ -118,6 +119,25 @@ export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectCl )} + {/* Visual Scripting Button */} + {onVisualScriptingClick && ( + + + + + Visual Scripting (Node Editor) + + )} +
@@ -238,6 +258,12 @@ export function Toolbar({ code, onTemplatesClick, onPreviewClick, onNewProjectCl Avatar Toolkit )} + {onVisualScriptingClick && ( + + + Visual Scripting + + )} Copy Code diff --git a/src/components/visual-scripting/VisualScriptingCanvas.tsx b/src/components/visual-scripting/VisualScriptingCanvas.tsx new file mode 100644 index 0000000..02eca49 --- /dev/null +++ b/src/components/visual-scripting/VisualScriptingCanvas.tsx @@ -0,0 +1,447 @@ +'use client'; + +import { useCallback, useRef, useState, useMemo } from 'react'; +import ReactFlow, { + Background, + Controls, + MiniMap, + Node, + Edge, + Connection, + ReactFlowProvider, + useReactFlow, + Panel, + BackgroundVariant, +} from 'reactflow'; +import 'reactflow/dist/style.css'; + +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { ScrollArea } from '@/components/ui/scroll-area'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Separator } from '@/components/ui/separator'; +import { Input } from '@/components/ui/input'; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + Play, + Code, + Trash2, + Undo, + Redo, + Save, + Download, + Upload, + Search, + Zap, + GitBranch, + Box, + Hash, + Globe, + Settings, + Copy, + Check, + AlertTriangle, +} from 'lucide-react'; + +import { useVisualScriptStore } from '@/stores/visual-script-store'; +import { + ALL_NODES, + CATEGORY_COLORS, + NodeCategory, + NodeDefinition, + getNodesByCategory, +} from '@/lib/visual-scripting/node-definitions'; +import { + generateCode, + validateScript, + Platform, + NodeData, +} from '@/lib/visual-scripting/code-generator'; +import { CustomNode } from './nodes/CustomNode'; +import { toast } from 'sonner'; + +// Custom node types +const nodeTypes = { + custom: CustomNode, +}; + +// Category icons +const CATEGORY_ICONS: Record = { + events: , + logic: , + actions: , + data: , + references: , + custom: , +}; + +interface VisualScriptingCanvasProps { + platform: Platform; + onCodeGenerated?: (code: string) => void; +} + +function VisualScriptingCanvasInner({ + platform, + onCodeGenerated, +}: VisualScriptingCanvasProps) { + const reactFlowWrapper = useRef(null); + const { screenToFlowPosition } = useReactFlow(); + + const { + nodes, + edges, + onNodesChange, + onEdgesChange, + onConnect, + addNode, + clearScript, + undo, + redo, + setGeneratedCode, + generatedCode, + history, + historyIndex, + } = useVisualScriptStore(); + + const [searchQuery, setSearchQuery] = useState(''); + const [showCodePreview, setShowCodePreview] = useState(false); + const [validationResult, setValidationResult] = useState<{ + valid: boolean; + errors: string[]; + warnings: string[]; + } | null>(null); + const [copied, setCopied] = useState(false); + + // Filter nodes by search and category + const filteredNodes = useMemo(() => { + if (!searchQuery) return ALL_NODES; + const query = searchQuery.toLowerCase(); + return ALL_NODES.filter( + (n) => + n.label.toLowerCase().includes(query) || + n.description.toLowerCase().includes(query) || + n.category.toLowerCase().includes(query) + ); + }, [searchQuery]); + + // Handle node drop from palette + const onDrop = useCallback( + (event: React.DragEvent) => { + event.preventDefault(); + + const nodeType = event.dataTransfer.getData('application/reactflow'); + if (!nodeType) return; + + const nodeDef = ALL_NODES.find((n) => n.type === nodeType); + if (!nodeDef) return; + + const position = screenToFlowPosition({ + x: event.clientX, + y: event.clientY, + }); + + const newNode: Node = { + id: `${nodeType}-${Date.now()}`, + type: 'custom', + position, + data: { + type: nodeType, + label: nodeDef.label, + values: {}, + }, + }; + + addNode(newNode); + }, + [screenToFlowPosition, addNode] + ); + + const onDragOver = useCallback((event: React.DragEvent) => { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + }, []); + + // Generate code from nodes + const handleGenerateCode = useCallback(() => { + const result = generateCode(nodes, edges, platform); + setGeneratedCode(result.code); + onCodeGenerated?.(result.code); + + const validation = validateScript(nodes, edges, platform); + setValidationResult(validation); + + if (result.errors.length > 0) { + toast.error(`Generation errors: ${result.errors.join(', ')}`); + } else if (result.warnings.length > 0) { + toast.warning(`Warnings: ${result.warnings.join(', ')}`); + } else { + toast.success('Code generated successfully!'); + } + + setShowCodePreview(true); + }, [nodes, edges, platform, setGeneratedCode, onCodeGenerated]); + + // Copy code to clipboard + const handleCopyCode = useCallback(async () => { + await navigator.clipboard.writeText(generatedCode); + setCopied(true); + toast.success('Code copied to clipboard!'); + setTimeout(() => setCopied(false), 2000); + }, [generatedCode]); + + // Drag start from palette + const onDragStart = (event: React.DragEvent, nodeType: string) => { + event.dataTransfer.setData('application/reactflow', nodeType); + event.dataTransfer.effectAllowed = 'move'; + }; + + const categories: NodeCategory[] = [ + 'events', + 'logic', + 'actions', + 'data', + 'references', + ]; + + return ( +
+ {/* Node Palette (Left Panel) */} +
+
+
+ + setSearchQuery(e.target.value)} + className="pl-9 h-9" + /> +
+
+ + + + {categories.map((cat) => ( + + {CATEGORY_ICONS[cat]} + + ))} + + + + {categories.map((category) => ( + +
+ {(searchQuery + ? filteredNodes.filter((n) => n.category === category) + : getNodesByCategory(category) + ) + .filter((n) => n.platforms.includes(platform)) + .map((nodeDef) => ( + + ))} +
+
+ ))} +
+
+
+ + {/* Main Canvas */} +
+ + + + { + const def = ALL_NODES.find((n) => n.type === node.data?.type); + return def?.color || '#6b7280'; + }} + className="!bg-card" + /> + + {/* Top Toolbar */} + + + + + + + + + + {/* Platform Badge */} + + + {platform.toUpperCase()} + + + + {/* Stats */} + + {nodes.length} nodes • {edges.length} connections + + +
+ + {/* Code Preview Dialog */} + + + + + + Generated Code + {validationResult && ( + + {validationResult.valid ? 'Valid' : 'Has Issues'} + + )} + + + + {validationResult && validationResult.errors.length > 0 && ( +
+

+ + Errors +

+
    + {validationResult.errors.map((err, i) => ( +
  • {err}
  • + ))} +
+
+ )} + + {validationResult && validationResult.warnings.length > 0 && ( +
+

+ + Warnings +

+
    + {validationResult.warnings.map((warn, i) => ( +
  • {warn}
  • + ))} +
+
+ )} + + +
+              {generatedCode}
+            
+
+ +
+ + +
+
+
+
+ ); +} + +// Node Palette Item Component +function NodePaletteItem({ + node, + onDragStart, +}: { + node: NodeDefinition; + onDragStart: (event: React.DragEvent, nodeType: string) => void; +}) { + return ( +
onDragStart(e, node.type)} + className="flex items-center gap-2 p-2 rounded-md border bg-background hover:bg-accent cursor-grab active:cursor-grabbing transition-colors" + style={{ borderLeftColor: node.color, borderLeftWidth: 3 }} + > +
+ {CATEGORY_ICONS[node.category]} +
+
+

{node.label}

+

+ {node.description} +

+
+
+ ); +} + +// Export with provider wrapper +export default function VisualScriptingCanvas( + props: VisualScriptingCanvasProps +) { + return ( + + + + ); +} diff --git a/src/components/visual-scripting/nodes/CustomNode.tsx b/src/components/visual-scripting/nodes/CustomNode.tsx new file mode 100644 index 0000000..7a4242b --- /dev/null +++ b/src/components/visual-scripting/nodes/CustomNode.tsx @@ -0,0 +1,326 @@ +'use client'; + +import { memo, useState } from 'react'; +import { Handle, Position, NodeProps } from 'reactflow'; +import { Input } from '@/components/ui/input'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Trash2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +import { + getNodeDefinition, + CATEGORY_COLORS, + PORT_COLORS, + NodePort, +} from '@/lib/visual-scripting/node-definitions'; +import { NodeData } from '@/lib/visual-scripting/code-generator'; +import { useVisualScriptStore } from '@/stores/visual-script-store'; + +export const CustomNode = memo(({ id, data, selected }: NodeProps) => { + const definition = getNodeDefinition(data.type); + const { updateNodeValue, removeNode } = useVisualScriptStore(); + const [isHovered, setIsHovered] = useState(false); + + if (!definition) { + return ( +
+ Unknown node: {data.type} +
+ ); + } + + const handleValueChange = (key: string, value: any) => { + updateNodeValue(id, key, value); + }; + + // Filter out flow inputs for the input section + const editableInputs = definition.inputs.filter( + (input) => input.type !== 'flow' + ); + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {/* Header */} +
+
+
+ {definition.category.charAt(0).toUpperCase()} +
+ {definition.label} +
+ {isHovered && ( + + )} +
+ + {/* Input Handles (Left Side) */} +
+ {definition.inputs.map((input, index) => ( +
+ 0 ? 45 : 20) + + index * 28 + + (input.type === 'flow' ? 0 : editableInputs.indexOf(input) * 36) + }px`, + }} + isConnectable={true} + /> + {input.type === 'flow' && ( +
+ {input.name} +
+ )} +
+ ))} +
+ + {/* Editable Inputs */} + {editableInputs.length > 0 && ( +
+ {editableInputs.map((input) => ( + handleValueChange(input.id, value)} + /> + ))} +
+ )} + + {/* Output Handles (Right Side) */} +
+ {definition.outputs.map((output, index) => ( +
+ +
+ {output.name} +
+
+ ))} +
+ + {/* Spacer for outputs */} +
+
+ ); +}); + +CustomNode.displayName = 'CustomNode'; + +// Input field component for node properties +function NodeInput({ + input, + value, + onChange, +}: { + input: NodePort; + value: any; + onChange: (value: any) => void; +}) { + switch (input.type) { + case 'number': + return ( +
+ + onChange(parseFloat(e.target.value) || 0)} + className="h-7 text-xs" + /> +
+ ); + + case 'string': + if (input.id === 'operation') { + return ( +
+ + +
+ ); + } + if (input.id === 'comparison') { + return ( +
+ + +
+ ); + } + if (input.id === 'key') { + return ( +
+ + +
+ ); + } + if (input.id === 'service') { + return ( +
+ + +
+ ); + } + return ( +
+ + onChange(e.target.value)} + className="h-7 text-xs" + placeholder={input.name} + /> +
+ ); + + case 'boolean': + return ( +
+ + +
+ ); + + default: + return ( +
+ + onChange(e.target.value)} + className="h-7 text-xs" + placeholder={`${input.name} (${input.type})`} + /> +
+ ); + } +} diff --git a/src/lib/visual-scripting/code-generator.ts b/src/lib/visual-scripting/code-generator.ts new file mode 100644 index 0000000..7828425 --- /dev/null +++ b/src/lib/visual-scripting/code-generator.ts @@ -0,0 +1,553 @@ +/** + * AeThex Visual Scripting - Code Generator + * Converts visual script nodes into platform-specific code + */ + +import { Node, Edge } from 'reactflow'; +import { + getNodeDefinition, + NodeDefinition, + PortType, + ALL_NODES +} from './node-definitions'; + +export type Platform = 'roblox' | 'uefn' | 'spatial'; + +export interface GenerationResult { + success: boolean; + code: string; + errors: string[]; + warnings: string[]; + nodeCount: number; + connectionCount: number; +} + +export interface NodeData { + type: string; + label: string; + values: Record; +} + +// Track visited nodes to prevent infinite loops +type VisitedSet = Set; + +/** + * Generate code from visual script nodes + */ +export function generateCode( + nodes: Node[], + edges: Edge[], + platform: Platform +): GenerationResult { + const result: GenerationResult = { + success: true, + code: '', + errors: [], + warnings: [], + nodeCount: nodes.length, + connectionCount: edges.length, + }; + + if (nodes.length === 0) { + result.code = getEmptyTemplate(platform); + return result; + } + + // Find all event nodes (entry points) + const eventNodes = nodes.filter(n => { + const def = getNodeDefinition(n.data.type); + return def?.category === 'events'; + }); + + if (eventNodes.length === 0) { + result.warnings.push('No event nodes found. Add an event node to start your script.'); + result.code = getEmptyTemplate(platform); + return result; + } + + // Build adjacency map for traversal + const adjacencyMap = buildAdjacencyMap(edges); + const reverseAdjacencyMap = buildReverseAdjacencyMap(edges); + + // Generate code for each event node + const codeBlocks: string[] = []; + + for (const eventNode of eventNodes) { + try { + const visited: VisitedSet = new Set(); + const blockCode = generateNodeCode( + eventNode, + nodes, + adjacencyMap, + reverseAdjacencyMap, + platform, + visited, + 0 + ); + codeBlocks.push(blockCode); + } catch (error: any) { + result.errors.push(`Error in node "${eventNode.data.label}": ${error.message}`); + result.success = false; + } + } + + // Combine code blocks + result.code = wrapCode(codeBlocks.join('\n\n'), platform); + + return result; +} + +/** + * Build adjacency map from edges (source -> targets) + */ +function buildAdjacencyMap(edges: Edge[]): Map { + const map = new Map(); + + for (const edge of edges) { + const key = `${edge.source}.${edge.sourceHandle}`; + if (!map.has(key)) { + map.set(key, []); + } + map.get(key)!.push(edge); + } + + return map; +} + +/** + * Build reverse adjacency map (target -> sources) + */ +function buildReverseAdjacencyMap(edges: Edge[]): Map { + const map = new Map(); + + for (const edge of edges) { + const key = `${edge.target}.${edge.targetHandle}`; + if (!map.has(key)) { + map.set(key, []); + } + map.get(key)!.push(edge); + } + + return map; +} + +/** + * Generate code for a single node and its connected nodes + */ +function generateNodeCode( + node: Node, + allNodes: Node[], + adjacencyMap: Map, + reverseMap: Map, + platform: Platform, + visited: VisitedSet, + depth: number +): string { + if (visited.has(node.id)) { + return '-- [Circular reference detected]'; + } + visited.add(node.id); + + const definition = getNodeDefinition(node.data.type); + if (!definition) { + return `-- Unknown node type: ${node.data.type}`; + } + + const template = definition.codeTemplate[platform]; + if (!template) { + return `-- Node "${definition.label}" not supported on ${platform}`; + } + + // Resolve input values + const resolvedValues: Record = {}; + + for (const input of definition.inputs) { + // Check if there's a connection to this input + const connectionKey = `${node.id}.${input.id}`; + const incomingEdges = reverseMap.get(connectionKey) || []; + + if (incomingEdges.length > 0 && input.type !== 'flow') { + // Get value from connected node + const sourceEdge = incomingEdges[0]; + const sourceNode = allNodes.find(n => n.id === sourceEdge.source); + + if (sourceNode) { + resolvedValues[input.id] = generateValueExpression( + sourceNode, + sourceEdge.sourceHandle || 'value', + allNodes, + reverseMap, + platform, + new Set(visited), + depth + 1 + ); + } + } else { + // Use node's stored value or default + const value = node.data.values?.[input.id] ?? input.defaultValue ?? getDefaultForType(input.type); + resolvedValues[input.id] = formatValue(value, input.type, platform); + } + } + + // Generate body code (connected flow outputs) + let code = template; + + // Replace placeholders with values + for (const [key, value] of Object.entries(resolvedValues)) { + code = code.replace(new RegExp(`{{${key}}}`, 'g'), value); + } + + // Handle flow outputs (execution continues) + for (const output of definition.outputs) { + if (output.type === 'flow') { + const connectionKey = `${node.id}.${output.id}`; + const outgoingEdges = adjacencyMap.get(connectionKey) || []; + + let bodyCode = ''; + for (const edge of outgoingEdges) { + const targetNode = allNodes.find(n => n.id === edge.target); + if (targetNode) { + bodyCode += generateNodeCode( + targetNode, + allNodes, + adjacencyMap, + reverseMap, + platform, + visited, + depth + 1 + ); + } + } + + // Replace body placeholder + const bodyPlaceholder = getBodyPlaceholder(output.id); + code = code.replace(new RegExp(bodyPlaceholder, 'g'), indent(bodyCode, depth + 1)); + } + } + + // Clean up any remaining placeholders + code = code.replace(/{{[A-Z_]+_BODY}}/g, ''); + code = code.replace(/{{BODY}}/g, ''); + + return code; +} + +/** + * Generate a value expression for a data node + */ +function generateValueExpression( + node: Node, + outputHandle: string, + allNodes: Node[], + reverseMap: Map, + platform: Platform, + visited: VisitedSet, + depth: number +): string { + if (visited.has(node.id)) { + return 'nil'; + } + visited.add(node.id); + + const definition = getNodeDefinition(node.data.type); + if (!definition) { + return 'nil'; + } + + const template = definition.codeTemplate[platform]; + if (!template) { + return 'nil'; + } + + // Resolve input values recursively + let code = template; + + for (const input of definition.inputs) { + const connectionKey = `${node.id}.${input.id}`; + const incomingEdges = reverseMap.get(connectionKey) || []; + + let value: string; + if (incomingEdges.length > 0 && input.type !== 'flow') { + const sourceEdge = incomingEdges[0]; + const sourceNode = allNodes.find(n => n.id === sourceEdge.source); + + if (sourceNode) { + value = generateValueExpression( + sourceNode, + sourceEdge.sourceHandle || 'value', + allNodes, + reverseMap, + platform, + visited, + depth + 1 + ); + } else { + value = formatValue(input.defaultValue, input.type, platform); + } + } else { + const nodeValue = node.data.values?.[input.id] ?? input.defaultValue ?? getDefaultForType(input.type); + value = formatValue(nodeValue, input.type, platform); + } + + code = code.replace(new RegExp(`{{${input.id}}}`, 'g'), value); + } + + // Handle special operations + if (node.data.type === 'math') { + const op = node.data.values?.operation || 'add'; + const opSymbol = getOperationSymbol(op); + code = code.replace('{{OPERATION}}', opSymbol); + } + + if (node.data.type === 'compare') { + const comp = node.data.values?.comparison || 'equals'; + const compSymbol = getComparisonSymbol(comp, platform); + code = code.replace('{{COMPARISON}}', compSymbol); + } + + return code; +} + +/** + * Format a value for the target platform + */ +function formatValue(value: any, type: PortType, platform: Platform): string { + if (value === null || value === undefined) { + return getDefaultForType(type); + } + + switch (type) { + case 'string': + return `"${String(value).replace(/"/g, '\\"')}"`; + case 'number': + return String(Number(value) || 0); + case 'boolean': + if (platform === 'roblox') { + return value ? 'true' : 'false'; + } else if (platform === 'uefn') { + return value ? 'true' : 'false'; + } + return value ? 'true' : 'false'; + default: + return String(value); + } +} + +/** + * Get default value for a port type + */ +function getDefaultForType(type: PortType): string { + switch (type) { + case 'number': return '0'; + case 'string': return '""'; + case 'boolean': return 'false'; + case 'object': return 'nil'; + case 'array': return '{}'; + default: return 'nil'; + } +} + +/** + * Get the body placeholder name for an output + */ +function getBodyPlaceholder(outputId: string): string { + const mapping: Record = { + 'flow': '{{BODY}}', + 'true': '{{TRUE_BODY}}', + 'false': '{{FALSE_BODY}}', + 'loop': '{{LOOP_BODY}}', + 'complete': '{{COMPLETE_BODY}}', + }; + return mapping[outputId] || '{{BODY}}'; +} + +/** + * Get math operation symbol + */ +function getOperationSymbol(operation: string): string { + const symbols: Record = { + 'add': '+', + 'subtract': '-', + 'multiply': '*', + 'divide': '/', + 'modulo': '%', + 'power': '^', + }; + return symbols[operation] || '+'; +} + +/** + * Get comparison symbol for platform + */ +function getComparisonSymbol(comparison: string, platform: Platform): string { + const symbols: Record = { + 'equals': '==', + 'notEquals': platform === 'roblox' ? '~=' : '!=', + 'greater': '>', + 'less': '<', + 'greaterEqual': '>=', + 'lessEqual': '<=', + }; + return symbols[comparison] || '=='; +} + +/** + * Indent code block + */ +function indent(code: string, level: number): string { + const tab = '\t'; + const prefix = tab.repeat(level); + return code + .split('\n') + .map(line => line.trim() ? prefix + line : line) + .join('\n'); +} + +/** + * Wrap generated code with platform boilerplate + */ +function wrapCode(code: string, platform: Platform): string { + switch (platform) { + case 'roblox': + return `-- Generated by AeThex Visual Scripting +-- Platform: Roblox (Lua) + +${code}`; + + case 'uefn': + return `# Generated by AeThex Visual Scripting +# Platform: UEFN (Verse) + +using { /Fortnite.com/Devices } +using { /Verse.org/Simulation } +using { /UnrealEngine.com/Temporary/Diagnostics } + +aethex_generated_device := class(creative_device): + +${indent(code, 1)}`; + + case 'spatial': + return `// Generated by AeThex Visual Scripting +// Platform: Spatial (TypeScript) + +import { SpaceService, World } from '@spatial/core'; + +${code}`; + + default: + return code; + } +} + +/** + * Get empty template for a platform + */ +function getEmptyTemplate(platform: Platform): string { + switch (platform) { + case 'roblox': + return `-- Generated by AeThex Visual Scripting +-- Add event nodes to start building your script! + +-- Example: +-- 1. Drag an "On Player Join" event node +-- 2. Connect it to an action like "Print" +-- 3. Click "Generate Code" + +print("Hello from AeThex!")`; + + case 'uefn': + return `# Generated by AeThex Visual Scripting +# Add event nodes to start building your script! + +using { /Fortnite.com/Devices } +using { /Verse.org/Simulation } + +aethex_generated_device := class(creative_device): + OnBegin(): void = + Print("Hello from AeThex!")`; + + case 'spatial': + return `// Generated by AeThex Visual Scripting +// Add event nodes to start building your script! + +import { SpaceService } from '@spatial/core'; + +SpaceService.onSpaceReady.on(() => { + console.log("Hello from AeThex!"); +});`; + + default: + return '// No code generated'; + } +} + +/** + * Validate visual script for errors + */ +export function validateScript( + nodes: Node[], + edges: Edge[], + platform: Platform +): { valid: boolean; errors: string[]; warnings: string[] } { + const errors: string[] = []; + const warnings: string[] = []; + + // Check for event nodes + const eventNodes = nodes.filter(n => { + const def = getNodeDefinition(n.data.type); + return def?.category === 'events'; + }); + + if (eventNodes.length === 0) { + warnings.push('No event nodes found. Your script needs at least one event to run.'); + } + + // Check for disconnected nodes + const connectedNodes = new Set(); + for (const edge of edges) { + connectedNodes.add(edge.source); + connectedNodes.add(edge.target); + } + + for (const node of nodes) { + if (!connectedNodes.has(node.id) && eventNodes.findIndex(e => e.id === node.id) === -1) { + const def = getNodeDefinition(node.data.type); + if (def?.category !== 'events') { + warnings.push(`Node "${node.data.label || node.data.type}" is not connected.`); + } + } + } + + // Check for required inputs + for (const node of nodes) { + const def = getNodeDefinition(node.data.type); + if (!def) continue; + + for (const input of def.inputs) { + if (input.required && input.type !== 'flow') { + const hasConnection = edges.some( + e => e.target === node.id && e.targetHandle === input.id + ); + const hasValue = node.data.values?.[input.id] !== undefined; + + if (!hasConnection && !hasValue) { + errors.push(`Node "${node.data.label || def.label}" is missing required input: ${input.name}`); + } + } + } + } + + // Check for platform compatibility + for (const node of nodes) { + const def = getNodeDefinition(node.data.type); + if (def && !def.platforms.includes(platform)) { + errors.push(`Node "${def.label}" is not available on ${platform}`); + } + } + + return { + valid: errors.length === 0, + errors, + warnings, + }; +} diff --git a/src/lib/visual-scripting/node-definitions.ts b/src/lib/visual-scripting/node-definitions.ts new file mode 100644 index 0000000..bbcd1c0 --- /dev/null +++ b/src/lib/visual-scripting/node-definitions.ts @@ -0,0 +1,828 @@ +/** + * AeThex Visual Scripting - Node Definitions + * All available nodes for the visual scripting system + */ + +import { Node, Edge } from 'reactflow'; + +// Node Categories +export type NodeCategory = + | 'events' // Entry points (green) + | 'logic' // Control flow (blue) + | 'actions' // Do things (purple) + | 'data' // Values and operations (orange) + | 'references' // Game objects (yellow) + | 'custom'; // User-defined (gray) + +// Port types for connections +export type PortType = + | 'flow' // Execution flow (white) + | 'number' // Number values (blue) + | 'string' // Text values (pink) + | 'boolean' // True/false (red) + | 'object' // Game objects (yellow) + | 'array' // Lists (green) + | 'any'; // Any type (gray) + +export interface NodePort { + id: string; + name: string; + type: PortType; + defaultValue?: any; + required?: boolean; +} + +export interface NodeDefinition { + type: string; + category: NodeCategory; + label: string; + description: string; + icon: string; + color: string; + inputs: NodePort[]; + outputs: NodePort[]; + platforms: ('roblox' | 'uefn' | 'spatial')[]; + codeTemplate: { + roblox?: string; + uefn?: string; + spatial?: string; + }; +} + +// Color mapping for categories +export const CATEGORY_COLORS: Record = { + events: '#22c55e', // Green + logic: '#3b82f6', // Blue + actions: '#a855f7', // Purple + data: '#f97316', // Orange + references: '#eab308', // Yellow + custom: '#6b7280', // Gray +}; + +// Color mapping for port types +export const PORT_COLORS: Record = { + flow: '#ffffff', + number: '#3b82f6', + string: '#ec4899', + boolean: '#ef4444', + object: '#eab308', + array: '#22c55e', + any: '#6b7280', +}; + +// ============================================ +// EVENT NODES - Entry points for scripts +// ============================================ + +export const EVENT_NODES: NodeDefinition[] = [ + { + type: 'onPlayerJoin', + category: 'events', + label: 'On Player Join', + description: 'Triggered when a player joins the game', + icon: 'UserPlus', + color: CATEGORY_COLORS.events, + inputs: [], + outputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'player', name: 'Player', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `game.Players.PlayerAdded:Connect(function(player)\n{{BODY}}\nend)`, + uefn: `OnPlayerAdded(): void =\n{{BODY}}`, + spatial: `SpaceService.onPlayerJoined.on((player) => {\n{{BODY}}\n});`, + }, + }, + { + type: 'onPlayerLeave', + category: 'events', + label: 'On Player Leave', + description: 'Triggered when a player leaves the game', + icon: 'UserMinus', + color: CATEGORY_COLORS.events, + inputs: [], + outputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'player', name: 'Player', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `game.Players.PlayerRemoving:Connect(function(player)\n{{BODY}}\nend)`, + uefn: `OnPlayerRemoved(): void =\n{{BODY}}`, + spatial: `SpaceService.onPlayerLeft.on((player) => {\n{{BODY}}\n});`, + }, + }, + { + type: 'onPartTouch', + category: 'events', + label: 'On Part Touched', + description: 'Triggered when something touches a part', + icon: 'Hand', + color: CATEGORY_COLORS.events, + inputs: [ + { id: 'part', name: 'Part', type: 'object', required: true }, + ], + outputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'otherPart', name: 'Other Part', type: 'object' }, + { id: 'player', name: 'Player (if any)', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{part}}.Touched:Connect(function(otherPart)\n\tlocal player = game.Players:GetPlayerFromCharacter(otherPart.Parent)\n{{BODY}}\nend)`, + uefn: `{{part}}.OnBeginOverlap.Subscribe(function(OtherActor: actor):\n{{BODY}}`, + spatial: `{{part}}.onCollisionEnter.on((collision) => {\n{{BODY}}\n});`, + }, + }, + { + type: 'onKeyPress', + category: 'events', + label: 'On Key Press', + description: 'Triggered when a key is pressed', + icon: 'Keyboard', + color: CATEGORY_COLORS.events, + inputs: [ + { id: 'key', name: 'Key', type: 'string', defaultValue: 'E' }, + ], + outputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'player', name: 'Player', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `local UserInputService = game:GetService("UserInputService")\nUserInputService.InputBegan:Connect(function(input, gameProcessed)\n\tif not gameProcessed and input.KeyCode == Enum.KeyCode.{{key}} then\n{{BODY}}\n\tend\nend)`, + uefn: `OnKeyPressed(Key: keycode_{{key}}): void =\n{{BODY}}`, + spatial: `Input.onKeyDown("{{key}}", () => {\n{{BODY}}\n});`, + }, + }, + { + type: 'onTimer', + category: 'events', + label: 'On Timer', + description: 'Triggers repeatedly at an interval', + icon: 'Clock', + color: CATEGORY_COLORS.events, + inputs: [ + { id: 'interval', name: 'Interval (sec)', type: 'number', defaultValue: 1 }, + ], + outputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `while true do\n\ttask.wait({{interval}})\n{{BODY}}\nend`, + uefn: `loop:\n\tSleep({{interval}})\n{{BODY}}`, + spatial: `setInterval(() => {\n{{BODY}}\n}, {{interval}} * 1000);`, + }, + }, + { + type: 'onGameStart', + category: 'events', + label: 'On Game Start', + description: 'Runs once when the game starts', + icon: 'Play', + color: CATEGORY_COLORS.events, + inputs: [], + outputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `-- Runs on game start\n{{BODY}}`, + uefn: `OnBegin(): void =\n{{BODY}}`, + spatial: `SpaceService.onSpaceReady.on(() => {\n{{BODY}}\n});`, + }, + }, +]; + +// ============================================ +// LOGIC NODES - Control flow +// ============================================ + +export const LOGIC_NODES: NodeDefinition[] = [ + { + type: 'ifCondition', + category: 'logic', + label: 'If', + description: 'Branch based on a condition', + icon: 'GitBranch', + color: CATEGORY_COLORS.logic, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'condition', name: 'Condition', type: 'boolean', required: true }, + ], + outputs: [ + { id: 'true', name: 'True', type: 'flow' }, + { id: 'false', name: 'False', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `if {{condition}} then\n{{TRUE_BODY}}\nelse\n{{FALSE_BODY}}\nend`, + uefn: `if ({{condition}}):\n{{TRUE_BODY}}\nelse:\n{{FALSE_BODY}}`, + spatial: `if ({{condition}}) {\n{{TRUE_BODY}}\n} else {\n{{FALSE_BODY}}\n}`, + }, + }, + { + type: 'forLoop', + category: 'logic', + label: 'For Loop', + description: 'Repeat a number of times', + icon: 'Repeat', + color: CATEGORY_COLORS.logic, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'start', name: 'Start', type: 'number', defaultValue: 1 }, + { id: 'end', name: 'End', type: 'number', defaultValue: 10 }, + ], + outputs: [ + { id: 'loop', name: 'Loop Body', type: 'flow' }, + { id: 'index', name: 'Index', type: 'number' }, + { id: 'complete', name: 'Completed', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `for i = {{start}}, {{end}} do\n{{LOOP_BODY}}\nend\n{{COMPLETE_BODY}}`, + uefn: `for (Index := {{start}}..{{end}}):\n{{LOOP_BODY}}\n{{COMPLETE_BODY}}`, + spatial: `for (let i = {{start}}; i <= {{end}}; i++) {\n{{LOOP_BODY}}\n}\n{{COMPLETE_BODY}}`, + }, + }, + { + type: 'forEach', + category: 'logic', + label: 'For Each', + description: 'Loop through a list', + icon: 'List', + color: CATEGORY_COLORS.logic, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'array', name: 'Array', type: 'array', required: true }, + ], + outputs: [ + { id: 'loop', name: 'Loop Body', type: 'flow' }, + { id: 'item', name: 'Current Item', type: 'any' }, + { id: 'index', name: 'Index', type: 'number' }, + { id: 'complete', name: 'Completed', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `for index, item in ipairs({{array}}) do\n{{LOOP_BODY}}\nend\n{{COMPLETE_BODY}}`, + uefn: `for (Index -> Item : {{array}}):\n{{LOOP_BODY}}\n{{COMPLETE_BODY}}`, + spatial: `{{array}}.forEach((item, index) => {\n{{LOOP_BODY}}\n});\n{{COMPLETE_BODY}}`, + }, + }, + { + type: 'wait', + category: 'logic', + label: 'Wait', + description: 'Pause execution for a duration', + icon: 'Clock', + color: CATEGORY_COLORS.logic, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'duration', name: 'Seconds', type: 'number', defaultValue: 1 }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `task.wait({{duration}})\n{{BODY}}`, + uefn: `Sleep({{duration}})\n{{BODY}}`, + spatial: `await delay({{duration}} * 1000);\n{{BODY}}`, + }, + }, + { + type: 'whileLoop', + category: 'logic', + label: 'While', + description: 'Loop while condition is true', + icon: 'RefreshCw', + color: CATEGORY_COLORS.logic, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'condition', name: 'Condition', type: 'boolean', required: true }, + ], + outputs: [ + { id: 'loop', name: 'Loop Body', type: 'flow' }, + { id: 'complete', name: 'Completed', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `while {{condition}} do\n{{LOOP_BODY}}\nend\n{{COMPLETE_BODY}}`, + uefn: `loop:\n\tif (not {{condition}}): break\n{{LOOP_BODY}}`, + spatial: `while ({{condition}}) {\n{{LOOP_BODY}}\n}\n{{COMPLETE_BODY}}`, + }, + }, +]; + +// ============================================ +// ACTION NODES - Do things +// ============================================ + +export const ACTION_NODES: NodeDefinition[] = [ + { + type: 'print', + category: 'actions', + label: 'Print', + description: 'Print a message to the console', + icon: 'MessageSquare', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'message', name: 'Message', type: 'string', defaultValue: 'Hello!' }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `print({{message}})\n{{BODY}}`, + uefn: `Print({{message}})\n{{BODY}}`, + spatial: `console.log({{message}});\n{{BODY}}`, + }, + }, + { + type: 'setProperty', + category: 'actions', + label: 'Set Property', + description: 'Set a property on an object', + icon: 'Settings', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'object', name: 'Object', type: 'object', required: true }, + { id: 'property', name: 'Property', type: 'string', required: true }, + { id: 'value', name: 'Value', type: 'any', required: true }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{object}}.{{property}} = {{value}}\n{{BODY}}`, + uefn: `set {{object}}.{{property}} = {{value}}\n{{BODY}}`, + spatial: `{{object}}.{{property}} = {{value}};\n{{BODY}}`, + }, + }, + { + type: 'createPart', + category: 'actions', + label: 'Create Part', + description: 'Create a new part in the world', + icon: 'Box', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'position', name: 'Position', type: 'object' }, + { id: 'size', name: 'Size', type: 'object' }, + { id: 'color', name: 'Color', type: 'object' }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + { id: 'part', name: 'Created Part', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `local newPart = Instance.new("Part")\nnewPart.Position = {{position}}\nnewPart.Size = {{size}}\nnewPart.BrickColor = {{color}}\nnewPart.Parent = workspace\n{{BODY}}`, + uefn: `var NewProp := SpawnProp(DefaultProp)\nNewProp.SetTransform({{position}})\n{{BODY}}`, + spatial: `const newPart = createPart({{position}}, {{size}}, {{color}});\n{{BODY}}`, + }, + }, + { + type: 'destroy', + category: 'actions', + label: 'Destroy', + description: 'Destroy an object', + icon: 'Trash2', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'object', name: 'Object', type: 'object', required: true }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{object}}:Destroy()\n{{BODY}}`, + uefn: `{{object}}.Dispose()\n{{BODY}}`, + spatial: `{{object}}.destroy();\n{{BODY}}`, + }, + }, + { + type: 'playSound', + category: 'actions', + label: 'Play Sound', + description: 'Play a sound effect', + icon: 'Volume2', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'soundId', name: 'Sound ID', type: 'string', required: true }, + { id: 'volume', name: 'Volume', type: 'number', defaultValue: 1 }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `local sound = Instance.new("Sound")\nsound.SoundId = "rbxassetid://{{soundId}}"\nsound.Volume = {{volume}}\nsound.Parent = workspace\nsound:Play()\n{{BODY}}`, + uefn: `PlaySound({{soundId}}, {{volume}})\n{{BODY}}`, + spatial: `playSound("{{soundId}}", { volume: {{volume}} });\n{{BODY}}`, + }, + }, + { + type: 'tween', + category: 'actions', + label: 'Tween Property', + description: 'Smoothly animate a property', + icon: 'Sparkles', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'object', name: 'Object', type: 'object', required: true }, + { id: 'property', name: 'Property', type: 'string', required: true }, + { id: 'target', name: 'Target Value', type: 'any', required: true }, + { id: 'duration', name: 'Duration', type: 'number', defaultValue: 1 }, + ], + outputs: [ + { id: 'flow', name: 'On Complete', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `local TweenService = game:GetService("TweenService")\nlocal tween = TweenService:Create({{object}}, TweenInfo.new({{duration}}), {{{property}} = {{target}}})\ntween:Play()\ntween.Completed:Wait()\n{{BODY}}`, + uefn: `{{object}}.MoveAndRotateTo({{target}}, {{duration}})\n{{BODY}}`, + spatial: `animate({{object}}, { {{property}}: {{target}} }, {{duration}} * 1000).then(() => {\n{{BODY}}\n});`, + }, + }, + { + type: 'teleport', + category: 'actions', + label: 'Teleport Player', + description: 'Move a player to a position', + icon: 'Zap', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'player', name: 'Player', type: 'object', required: true }, + { id: 'position', name: 'Position', type: 'object', required: true }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{player}}.Character:SetPrimaryPartCFrame(CFrame.new({{position}}))\n{{BODY}}`, + uefn: `{{player}}.Respawn({{position}}, {{player}}.GetRotation())\n{{BODY}}`, + spatial: `{{player}}.teleportTo({{position}});\n{{BODY}}`, + }, + }, + { + type: 'giveItem', + category: 'actions', + label: 'Give Item', + description: 'Give an item/tool to a player', + icon: 'Gift', + color: CATEGORY_COLORS.actions, + inputs: [ + { id: 'flow', name: 'Execute', type: 'flow' }, + { id: 'player', name: 'Player', type: 'object', required: true }, + { id: 'item', name: 'Item', type: 'object', required: true }, + ], + outputs: [ + { id: 'flow', name: 'Continue', type: 'flow' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `local itemClone = {{item}}:Clone()\nitemClone.Parent = {{player}}.Backpack\n{{BODY}}`, + uefn: `GrantItem({{player}}, {{item}})\n{{BODY}}`, + spatial: `{{player}}.inventory.add({{item}});\n{{BODY}}`, + }, + }, +]; + +// ============================================ +// DATA NODES - Values and operations +// ============================================ + +export const DATA_NODES: NodeDefinition[] = [ + { + type: 'number', + category: 'data', + label: 'Number', + description: 'A number value', + icon: 'Hash', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'value', name: 'Value', type: 'number', defaultValue: 0 }, + ], + outputs: [ + { id: 'value', name: 'Value', type: 'number' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{value}}`, + uefn: `{{value}}`, + spatial: `{{value}}`, + }, + }, + { + type: 'string', + category: 'data', + label: 'Text', + description: 'A text value', + icon: 'Type', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'value', name: 'Value', type: 'string', defaultValue: '' }, + ], + outputs: [ + { id: 'value', name: 'Value', type: 'string' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `"{{value}}"`, + uefn: `"{{value}}"`, + spatial: `"{{value}}"`, + }, + }, + { + type: 'boolean', + category: 'data', + label: 'Boolean', + description: 'True or False', + icon: 'ToggleLeft', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'value', name: 'Value', type: 'boolean', defaultValue: true }, + ], + outputs: [ + { id: 'value', name: 'Value', type: 'boolean' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{value}}`, + uefn: `{{value}}`, + spatial: `{{value}}`, + }, + }, + { + type: 'vector3', + category: 'data', + label: 'Vector3', + description: '3D position/direction', + icon: 'Move3d', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'x', name: 'X', type: 'number', defaultValue: 0 }, + { id: 'y', name: 'Y', type: 'number', defaultValue: 0 }, + { id: 'z', name: 'Z', type: 'number', defaultValue: 0 }, + ], + outputs: [ + { id: 'vector', name: 'Vector', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `Vector3.new({{x}}, {{y}}, {{z}})`, + uefn: `vector3{X := {{x}}, Y := {{y}}, Z := {{z}}}`, + spatial: `new Vector3({{x}}, {{y}}, {{z}})`, + }, + }, + { + type: 'color', + category: 'data', + label: 'Color', + description: 'RGB color value', + icon: 'Palette', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'r', name: 'Red', type: 'number', defaultValue: 255 }, + { id: 'g', name: 'Green', type: 'number', defaultValue: 255 }, + { id: 'b', name: 'Blue', type: 'number', defaultValue: 255 }, + ], + outputs: [ + { id: 'color', name: 'Color', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `Color3.fromRGB({{r}}, {{g}}, {{b}})`, + uefn: `MakeColor({{r}}, {{g}}, {{b}})`, + spatial: `new Color({{r}}, {{g}}, {{b}})`, + }, + }, + { + type: 'math', + category: 'data', + label: 'Math', + description: 'Mathematical operation', + icon: 'Calculator', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'a', name: 'A', type: 'number', required: true }, + { id: 'b', name: 'B', type: 'number', required: true }, + { id: 'operation', name: 'Operation', type: 'string', defaultValue: 'add' }, + ], + outputs: [ + { id: 'result', name: 'Result', type: 'number' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `({{a}} {{OPERATION}} {{b}})`, + uefn: `({{a}} {{OPERATION}} {{b}})`, + spatial: `({{a}} {{OPERATION}} {{b}})`, + }, + }, + { + type: 'compare', + category: 'data', + label: 'Compare', + description: 'Compare two values', + icon: 'Scale', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'a', name: 'A', type: 'any', required: true }, + { id: 'b', name: 'B', type: 'any', required: true }, + { id: 'comparison', name: 'Comparison', type: 'string', defaultValue: 'equals' }, + ], + outputs: [ + { id: 'result', name: 'Result', type: 'boolean' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `({{a}} {{COMPARISON}} {{b}})`, + uefn: `({{a}} {{COMPARISON}} {{b}})`, + spatial: `({{a}} {{COMPARISON}} {{b}})`, + }, + }, + { + type: 'random', + category: 'data', + label: 'Random', + description: 'Random number in range', + icon: 'Dices', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'min', name: 'Min', type: 'number', defaultValue: 1 }, + { id: 'max', name: 'Max', type: 'number', defaultValue: 100 }, + ], + outputs: [ + { id: 'value', name: 'Value', type: 'number' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `math.random({{min}}, {{max}})`, + uefn: `GetRandomInt({{min}}, {{max}})`, + spatial: `Math.floor(Math.random() * ({{max}} - {{min}} + 1)) + {{min}}`, + }, + }, + { + type: 'variable', + category: 'data', + label: 'Variable', + description: 'Store and retrieve a value', + icon: 'Variable', + color: CATEGORY_COLORS.data, + inputs: [ + { id: 'name', name: 'Name', type: 'string', required: true }, + { id: 'setValue', name: 'Set Value', type: 'any' }, + ], + outputs: [ + { id: 'getValue', name: 'Get Value', type: 'any' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{name}}`, + uefn: `{{name}}`, + spatial: `{{name}}`, + }, + }, +]; + +// ============================================ +// REFERENCE NODES - Game objects +// ============================================ + +export const REFERENCE_NODES: NodeDefinition[] = [ + { + type: 'getPlayer', + category: 'references', + label: 'Get Player', + description: 'Get a player by name or index', + icon: 'User', + color: CATEGORY_COLORS.references, + inputs: [ + { id: 'name', name: 'Name (optional)', type: 'string' }, + ], + outputs: [ + { id: 'player', name: 'Player', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `game.Players:FindFirstChild("{{name}}")`, + uefn: `GetPlayer[{{name}}]`, + spatial: `SpaceService.getPlayerByName("{{name}}")`, + }, + }, + { + type: 'getAllPlayers', + category: 'references', + label: 'Get All Players', + description: 'Get list of all players', + icon: 'Users', + color: CATEGORY_COLORS.references, + inputs: [], + outputs: [ + { id: 'players', name: 'Players', type: 'array' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `game.Players:GetPlayers()`, + uefn: `GetPlayspace().GetPlayers()`, + spatial: `SpaceService.getAllPlayers()`, + }, + }, + { + type: 'findChild', + category: 'references', + label: 'Find Child', + description: 'Find a child object by name', + icon: 'Search', + color: CATEGORY_COLORS.references, + inputs: [ + { id: 'parent', name: 'Parent', type: 'object', required: true }, + { id: 'name', name: 'Name', type: 'string', required: true }, + ], + outputs: [ + { id: 'child', name: 'Child', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `{{parent}}:FindFirstChild("{{name}}")`, + uefn: `{{parent}}.GetChildren().Find("{{name}}")`, + spatial: `{{parent}}.getChildByName("{{name}}")`, + }, + }, + { + type: 'workspace', + category: 'references', + label: 'Workspace', + description: 'The game workspace/world', + icon: 'Globe', + color: CATEGORY_COLORS.references, + inputs: [], + outputs: [ + { id: 'workspace', name: 'Workspace', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `workspace`, + uefn: `GetPlayspace()`, + spatial: `World`, + }, + }, + { + type: 'getService', + category: 'references', + label: 'Get Service', + description: 'Get a game service', + icon: 'Server', + color: CATEGORY_COLORS.references, + inputs: [ + { id: 'service', name: 'Service Name', type: 'string', required: true }, + ], + outputs: [ + { id: 'service', name: 'Service', type: 'object' }, + ], + platforms: ['roblox', 'uefn', 'spatial'], + codeTemplate: { + roblox: `game:GetService("{{service}}")`, + uefn: `Get{{service}}Service()`, + spatial: `{{service}}Service`, + }, + }, +]; + +// Combine all nodes +export const ALL_NODES: NodeDefinition[] = [ + ...EVENT_NODES, + ...LOGIC_NODES, + ...ACTION_NODES, + ...DATA_NODES, + ...REFERENCE_NODES, +]; + +// Get nodes by category +export function getNodesByCategory(category: NodeCategory): NodeDefinition[] { + return ALL_NODES.filter(n => n.category === category); +} + +// Get node by type +export function getNodeDefinition(type: string): NodeDefinition | undefined { + return ALL_NODES.find(n => n.type === type); +} + +// Get nodes for a specific platform +export function getNodesForPlatform(platform: 'roblox' | 'uefn' | 'spatial'): NodeDefinition[] { + return ALL_NODES.filter(n => n.platforms.includes(platform)); +} diff --git a/src/stores/visual-script-store.ts b/src/stores/visual-script-store.ts new file mode 100644 index 0000000..be599e7 --- /dev/null +++ b/src/stores/visual-script-store.ts @@ -0,0 +1,213 @@ +/** + * AeThex Visual Scripting - State Store + * Zustand store for managing visual script state + */ + +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; +import { + Node, + Edge, + Connection, + addEdge, + applyNodeChanges, + applyEdgeChanges, + NodeChange, + EdgeChange, +} from 'reactflow'; +import { NodeData } from '../lib/visual-scripting/code-generator'; + +interface VisualScriptState { + // Current script data + nodes: Node[]; + edges: Edge[]; + + // UI state + selectedNodes: string[]; + selectedEdges: string[]; + + // Generated code + generatedCode: string; + + // History for undo/redo + history: { nodes: Node[]; edges: Edge[] }[]; + historyIndex: number; + + // Actions + setNodes: (nodes: Node[]) => void; + setEdges: (edges: Edge[]) => void; + onNodesChange: (changes: NodeChange[]) => void; + onEdgesChange: (changes: EdgeChange[]) => void; + onConnect: (connection: Connection) => void; + addNode: (node: Node) => void; + removeNode: (nodeId: string) => void; + updateNodeData: (nodeId: string, data: Partial) => void; + updateNodeValue: (nodeId: string, key: string, value: any) => void; + setSelectedNodes: (nodeIds: string[]) => void; + setSelectedEdges: (edgeIds: string[]) => void; + setGeneratedCode: (code: string) => void; + clearScript: () => void; + saveToHistory: () => void; + undo: () => void; + redo: () => void; + loadScript: (nodes: Node[], edges: Edge[]) => void; +} + +export const useVisualScriptStore = create()( + persist( + (set, get) => ({ + nodes: [], + edges: [], + selectedNodes: [], + selectedEdges: [], + generatedCode: '', + history: [], + historyIndex: -1, + + setNodes: (nodes) => set({ nodes }), + + setEdges: (edges) => set({ edges }), + + onNodesChange: (changes) => { + set({ + nodes: applyNodeChanges(changes, get().nodes) as Node[], + }); + }, + + onEdgesChange: (changes) => { + set({ + edges: applyEdgeChanges(changes, get().edges), + }); + }, + + onConnect: (connection) => { + // Validate connection types could go here + set({ + edges: addEdge( + { + ...connection, + id: `edge-${Date.now()}`, + type: 'smoothstep', + animated: connection.sourceHandle === 'flow', + }, + get().edges + ), + }); + get().saveToHistory(); + }, + + addNode: (node) => { + set({ nodes: [...get().nodes, node] }); + get().saveToHistory(); + }, + + removeNode: (nodeId) => { + set({ + nodes: get().nodes.filter((n) => n.id !== nodeId), + edges: get().edges.filter( + (e) => e.source !== nodeId && e.target !== nodeId + ), + }); + get().saveToHistory(); + }, + + updateNodeData: (nodeId, data) => { + set({ + nodes: get().nodes.map((node) => + node.id === nodeId + ? { ...node, data: { ...node.data, ...data } } + : node + ), + }); + }, + + updateNodeValue: (nodeId, key, value) => { + set({ + nodes: get().nodes.map((node) => + node.id === nodeId + ? { + ...node, + data: { + ...node.data, + values: { + ...node.data.values, + [key]: value, + }, + }, + } + : node + ), + }); + }, + + setSelectedNodes: (nodeIds) => set({ selectedNodes: nodeIds }), + + setSelectedEdges: (edgeIds) => set({ selectedEdges: edgeIds }), + + setGeneratedCode: (code) => set({ generatedCode: code }), + + clearScript: () => { + set({ + nodes: [], + edges: [], + selectedNodes: [], + selectedEdges: [], + generatedCode: '', + }); + get().saveToHistory(); + }, + + saveToHistory: () => { + const { nodes, edges, history, historyIndex } = get(); + const newHistory = history.slice(0, historyIndex + 1); + newHistory.push({ nodes: [...nodes], edges: [...edges] }); + + // Keep only last 50 states + if (newHistory.length > 50) { + newHistory.shift(); + } + + set({ + history: newHistory, + historyIndex: newHistory.length - 1, + }); + }, + + undo: () => { + const { history, historyIndex } = get(); + if (historyIndex > 0) { + const prevState = history[historyIndex - 1]; + set({ + nodes: prevState.nodes, + edges: prevState.edges, + historyIndex: historyIndex - 1, + }); + } + }, + + redo: () => { + const { history, historyIndex } = get(); + if (historyIndex < history.length - 1) { + const nextState = history[historyIndex + 1]; + set({ + nodes: nextState.nodes, + edges: nextState.edges, + historyIndex: historyIndex + 1, + }); + } + }, + + loadScript: (nodes, edges) => { + set({ nodes, edges }); + get().saveToHistory(); + }, + }), + { + name: 'aethex-visual-script', + partialize: (state) => ({ + nodes: state.nodes, + edges: state.edges, + }), + } + ) +);