diff --git a/package-lock.json b/package-lock.json index 48e0979..c32a5e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "react-error-boundary": "^6.0.0", "react-hook-form": "^7.54.2", "react-resizable-panels": "^2.1.7", + "reactflow": "^11.11.4", "recharts": "^2.15.1", "sass": "^1.97.2", "sonner": "^2.0.1", @@ -3422,6 +3423,108 @@ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "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/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/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/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/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-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/@rolldown/pluginutils": { "version": "1.0.0-beta.47", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.47.tgz", @@ -4556,24 +4659,159 @@ "@types/node": "*" } }, + "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", @@ -4589,6 +4827,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", @@ -4598,6 +4854,18 @@ "@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.7", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", @@ -4613,12 +4881,37 @@ "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", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", @@ -4657,6 +4950,12 @@ "@types/send": "*" } }, + "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/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -5589,6 +5888,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/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -9174,6 +9479,24 @@ "react-dom": ">=16.6.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/readable-stream": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", @@ -10777,6 +11100,34 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "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 + } + } + }, "packages/spark-tools": { "name": "@github/spark", "version": "0.0.1", diff --git a/package.json b/package.json index d512791..6149b27 100644 --- a/package.json +++ b/package.json @@ -73,6 +73,7 @@ "react-error-boundary": "^6.0.0", "react-hook-form": "^7.54.2", "react-resizable-panels": "^2.1.7", + "reactflow": "^11.11.4", "recharts": "^2.15.1", "sass": "^1.97.2", "sonner": "^2.0.1", diff --git a/src/components/FeatureIdeaCloud.tsx b/src/components/FeatureIdeaCloud.tsx index 5132f72..d2c4104 100644 --- a/src/components/FeatureIdeaCloud.tsx +++ b/src/components/FeatureIdeaCloud.tsx @@ -1,5 +1,23 @@ -import { useState, useEffect, useRef } from 'react' +import { useState, useEffect, useCallback } from 'react' import { useKV } from '@github/spark/hooks' +import ReactFlow, { + Node, + Edge, + Controls, + Background, + BackgroundVariant, + useNodesState, + useEdgesState, + addEdge, + Connection as RFConnection, + MarkerType, + ConnectionMode, + Panel, + NodeProps, + Handle, + Position, +} from 'reactflow' +import 'reactflow/dist/style.css' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card } from '@/components/ui/card' @@ -7,20 +25,11 @@ import { Badge } from '@/components/ui/badge' import { Textarea } from '@/components/ui/textarea' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' -import { Plus, Trash, Sparkle, MagnifyingGlassMinus, MagnifyingGlassPlus, ArrowsOut, Hand, Link as LinkIcon, Selection, DotsThree, X } from '@phosphor-icons/react' -import { motion } from 'framer-motion' +import { Plus, Trash, Sparkle, DotsThree } from '@phosphor-icons/react' import { toast } from 'sonner' type ConnectionType = 'dependency' | 'association' | 'inheritance' | 'composition' | 'aggregation' -interface Connection { - id: string - fromId: string - toId: string - type: ConnectionType - label?: string -} - interface FeatureIdea { id: string title: string @@ -29,10 +38,11 @@ interface FeatureIdea { priority: 'low' | 'medium' | 'high' status: 'idea' | 'planned' | 'in-progress' | 'completed' createdAt: number - x: number - y: number - connectedTo?: string[] - connections?: Connection[] +} + +interface IdeaEdgeData { + type: ConnectionType + label?: string } const SEED_IDEAS: FeatureIdea[] = [ @@ -44,8 +54,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'completed', createdAt: Date.now() - 10000000, - x: 100, - y: 150, }, { id: 'idea-2', @@ -55,8 +63,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'idea', createdAt: Date.now() - 9000000, - x: 600, - y: 250, }, { id: 'idea-3', @@ -66,8 +72,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'idea', createdAt: Date.now() - 8000000, - x: 250, - y: 550, }, { id: 'idea-4', @@ -77,8 +81,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'planned', createdAt: Date.now() - 7000000, - x: 700, - y: 600, }, { id: 'idea-5', @@ -88,8 +90,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'idea', createdAt: Date.now() - 6000000, - x: 150, - y: 800, }, { id: 'idea-6', @@ -99,8 +99,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'idea', createdAt: Date.now() - 5000000, - x: 800, - y: 350, }, { id: 'idea-7', @@ -110,8 +108,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'low', status: 'completed', createdAt: Date.now() - 4000000, - x: 450, - y: 100, }, { id: 'idea-8', @@ -121,8 +117,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'in-progress', createdAt: Date.now() - 3000000, - x: 300, - y: 400, }, { id: 'idea-9', @@ -132,8 +126,6 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'medium', status: 'planned', createdAt: Date.now() - 2000000, - x: 550, - y: 750, }, { id: 'idea-10', @@ -143,28 +135,20 @@ const SEED_IDEAS: FeatureIdea[] = [ priority: 'high', status: 'idea', createdAt: Date.now() - 1000000, - x: 850, - y: 500, }, ] -const SEED_CONNECTIONS: Connection[] = [ - { id: 'conn-1', fromId: 'idea-1', toId: 'idea-8', type: 'dependency', label: 'requires' }, - { id: 'conn-2', fromId: 'idea-2', toId: 'idea-4', type: 'association', label: 'works with' }, - { id: 'conn-3', fromId: 'idea-8', toId: 'idea-5', type: 'composition', label: 'includes' }, -] - const CATEGORIES = ['AI/ML', 'Collaboration', 'Community', 'DevOps', 'Testing', 'Performance', 'Design', 'Database', 'Mobile', 'Accessibility', 'Productivity', 'Security', 'Analytics', 'Other'] const PRIORITIES = ['low', 'medium', 'high'] as const const STATUSES = ['idea', 'planned', 'in-progress', 'completed'] as const const CONNECTION_TYPES = ['dependency', 'association', 'inheritance', 'composition', 'aggregation'] as const const CONNECTION_STYLES = { - dependency: { stroke: 'hsl(var(--accent))', strokeDasharray: '8,4', arrowType: 'open' }, - association: { stroke: 'hsl(var(--primary))', strokeDasharray: '', arrowType: 'line' }, - inheritance: { stroke: 'hsl(var(--chart-2))', strokeDasharray: '', arrowType: 'hollow' }, - composition: { stroke: 'hsl(var(--destructive))', strokeDasharray: '', arrowType: 'diamond-filled' }, - aggregation: { stroke: 'hsl(var(--chart-4))', strokeDasharray: '', arrowType: 'diamond-hollow' }, + dependency: { stroke: 'hsl(var(--accent))', strokeDasharray: '8,4', markerEnd: MarkerType.ArrowClosed }, + association: { stroke: 'hsl(var(--primary))', strokeDasharray: '', markerEnd: MarkerType.Arrow }, + inheritance: { stroke: 'hsl(var(--chart-2))', strokeDasharray: '', markerEnd: MarkerType.ArrowClosed }, + composition: { stroke: 'hsl(var(--destructive))', strokeDasharray: '', markerEnd: MarkerType.ArrowClosed }, + aggregation: { stroke: 'hsl(var(--chart-4))', strokeDasharray: '', markerEnd: MarkerType.Arrow }, } const CONNECTION_LABELS = { @@ -188,44 +172,184 @@ const PRIORITY_COLORS = { high: 'border-red-400/60 bg-red-50/80 dark:bg-red-950/40', } +function IdeaNode({ data, selected }: NodeProps) { + return ( +
+ + + + + + +
+
+

{data.title}

+ +
+

+ {data.description} +

+
+ + {data.category} + + + {data.status} + +
+
+
+
+ ) +} + +const nodeTypes = { + ideaNode: IdeaNode, +} + export function FeatureIdeaCloud() { const [ideas, setIdeas] = useKV('feature-ideas', SEED_IDEAS) - const [connections, setConnections] = useKV('feature-connections', SEED_CONNECTIONS) + const [savedEdges, setSavedEdges] = useKV[]>('feature-idea-edges', [ + { + id: 'edge-1', + source: 'idea-1', + target: 'idea-8', + type: 'default', + animated: true, + data: { type: 'dependency', label: 'requires' }, + markerEnd: { type: MarkerType.ArrowClosed, color: 'hsl(var(--accent))' }, + style: { stroke: 'hsl(var(--accent))', strokeDasharray: '8,4' }, + }, + { + id: 'edge-2', + source: 'idea-2', + target: 'idea-4', + type: 'default', + data: { type: 'association', label: 'works with' }, + markerEnd: { type: MarkerType.Arrow, color: 'hsl(var(--primary))' }, + style: { stroke: 'hsl(var(--primary))' }, + }, + { + id: 'edge-3', + source: 'idea-8', + target: 'idea-5', + type: 'default', + data: { type: 'composition', label: 'includes' }, + markerEnd: { type: MarkerType.ArrowClosed, color: 'hsl(var(--destructive))' }, + style: { stroke: 'hsl(var(--destructive))' }, + }, + ]) + + const [nodes, setNodes, onNodesChange] = useNodesState([]) + const [edges, setEdges, onEdgesChange] = useEdgesState([]) const [selectedIdea, setSelectedIdea] = useState(null) - const [selectedConnection, setSelectedConnection] = useState(null) + const [selectedEdge, setSelectedEdge] = useState | null>(null) const [editDialogOpen, setEditDialogOpen] = useState(false) const [viewDialogOpen, setViewDialogOpen] = useState(false) - const [connectionDialogOpen, setConnectionDialogOpen] = useState(false) - const canvasRef = useRef(null) - const [zoom, setZoom] = useState(1) - const [pan, setPan] = useState({ x: 0, y: 0 }) - const [isPanning, setIsPanning] = useState(false) - const [panStart, setPanStart] = useState({ x: 0, y: 0 }) - const [draggedIdea, setDraggedIdea] = useState(null) - const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }) - const [tool, setTool] = useState<'select' | 'pan' | 'connect'>('select') - const [connectingFrom, setConnectingFrom] = useState(null) + const [edgeDialogOpen, setEdgeDialogOpen] = useState(false) const [connectionType, setConnectionType] = useState('association') - const [hoveredConnection, setHoveredConnection] = useState(null) - const [draggingConnection, setDraggingConnection] = useState<{ fromId: string, x: number, y: number } | null>(null) - const [hoveredNode, setHoveredNode] = useState(null) const safeIdeas = ideas || SEED_IDEAS - const safeConnections = connections || SEED_CONNECTIONS + const safeEdges = savedEdges || [] useEffect(() => { if (!ideas || ideas.length === 0) { setIdeas(SEED_IDEAS) } - if (!connections || connections.length === 0) { - setConnections(SEED_CONNECTIONS) + }, [ideas, setIdeas]) + + useEffect(() => { + const initialNodes: Node[] = safeIdeas.map((idea, index) => ({ + id: idea.id, + type: 'ideaNode', + position: { x: 100 + (index % 3) * 350, y: 100 + Math.floor(index / 3) * 250 }, + data: idea, + })) + setNodes(initialNodes) + }, [safeIdeas, setNodes]) + + useEffect(() => { + setEdges(safeEdges) + }, [safeEdges, setEdges]) + + useEffect(() => { + const handleEditIdea = (e: Event) => { + const customEvent = e as CustomEvent + setSelectedIdea(customEvent.detail) + setEditDialogOpen(true) } - }, [ideas, setIdeas, connections, setConnections]) + + window.addEventListener('editIdea', handleEditIdea) + return () => window.removeEventListener('editIdea', handleEditIdea) + }, []) + + const onConnect = useCallback( + (params: RFConnection) => { + if (!params.source || !params.target) return + + const style = CONNECTION_STYLES[connectionType] + const newEdge: Edge = { + id: `edge-${Date.now()}`, + source: params.source, + target: params.target, + sourceHandle: params.sourceHandle, + targetHandle: params.targetHandle, + type: 'default', + data: { type: connectionType, label: CONNECTION_LABELS[connectionType] }, + markerEnd: { type: style.markerEnd, color: style.stroke }, + style: { stroke: style.stroke, strokeDasharray: style.strokeDasharray }, + animated: connectionType === 'dependency', + } + + setEdges((eds) => { + const updatedEdges = addEdge(newEdge, eds) + setSavedEdges(updatedEdges) + return updatedEdges + }) + + toast.success('Ideas connected!') + }, + [connectionType, setEdges, setSavedEdges] + ) + + const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { + setSelectedEdge(edge) + setEdgeDialogOpen(true) + }, []) + + const onNodeDoubleClick = useCallback((event: React.MouseEvent, node: Node) => { + setSelectedIdea(node.data) + setViewDialogOpen(true) + }, []) const handleAddIdea = () => { - const canvasCenterX = (window.innerWidth / 2 - pan.x) / zoom - const canvasCenterY = (window.innerHeight / 2 - pan.y) / zoom - const newIdea: FeatureIdea = { id: `idea-${Date.now()}`, title: '', @@ -234,56 +358,11 @@ export function FeatureIdeaCloud() { priority: 'medium', status: 'idea', createdAt: Date.now(), - x: canvasCenterX, - y: canvasCenterY, } setSelectedIdea(newIdea) setEditDialogOpen(true) } - const handleEditIdea = (idea: FeatureIdea) => { - setSelectedIdea(idea) - setEditDialogOpen(true) - } - - const handleIdeaClick = (idea: FeatureIdea, e: React.MouseEvent) => { - if (tool === 'connect') { - e.stopPropagation() - if (!connectingFrom) { - setConnectingFrom(idea.id) - toast.info(`Click another idea to connect (${CONNECTION_LABELS[connectionType]})`) - } else if (connectingFrom !== idea.id) { - const existingConnection = safeConnections.find( - c => c.fromId === connectingFrom && c.toId === idea.id - ) - - if (existingConnection) { - toast.error('Connection already exists') - } else { - const newConnection: Connection = { - id: `conn-${Date.now()}`, - fromId: connectingFrom, - toId: idea.id, - type: connectionType, - label: CONNECTION_LABELS[connectionType], - } - setConnections((current) => [...(current || []), newConnection]) - toast.success('Ideas connected!') - } - setConnectingFrom(null) - } - return - } - } - - const handleIdeaDoubleClick = (idea: FeatureIdea, e: React.MouseEvent) => { - if (tool === 'select') { - e.stopPropagation() - setSelectedIdea(idea) - setViewDialogOpen(true) - } - } - const handleSaveIdea = () => { if (!selectedIdea || !selectedIdea.title.trim()) { toast.error('Please enter a title') @@ -299,6 +378,16 @@ export function FeatureIdeaCloud() { } }) + if (!(ideas || []).find(i => i.id === selectedIdea.id)) { + const newNode: Node = { + id: selectedIdea.id, + type: 'ideaNode', + position: { x: 400, y: 300 }, + data: selectedIdea, + } + setNodes((nds) => [...nds, newNode]) + } + setEditDialogOpen(false) setSelectedIdea(null) toast.success('Idea saved!') @@ -306,27 +395,43 @@ export function FeatureIdeaCloud() { const handleDeleteIdea = (id: string) => { setIdeas((currentIdeas) => (currentIdeas || []).filter(i => i.id !== id)) - setConnections((currentConnections) => - (currentConnections || []).filter(c => c.fromId !== id && c.toId !== id) - ) + setNodes((nds) => nds.filter(n => n.id !== id)) + + const updatedEdges = edges.filter(e => e.source !== id && e.target !== id) + setEdges(updatedEdges) + setSavedEdges(updatedEdges) + setEditDialogOpen(false) setViewDialogOpen(false) setSelectedIdea(null) toast.success('Idea deleted') } - const handleDeleteConnection = (connectionId: string) => { - setConnections((current) => (current || []).filter(c => c.id !== connectionId)) - setConnectionDialogOpen(false) - setSelectedConnection(null) + const handleDeleteEdge = (edgeId: string) => { + const updatedEdges = edges.filter(e => e.id !== edgeId) + setEdges(updatedEdges) + setSavedEdges(updatedEdges) + setEdgeDialogOpen(false) + setSelectedEdge(null) toast.success('Connection removed') } - const handleConnectionClick = (connection: Connection, e: React.MouseEvent) => { - if (tool === 'select') { - e.stopPropagation() - setSelectedConnection(connection) - setConnectionDialogOpen(true) + const handleSaveEdge = () => { + if (selectedEdge) { + const style = CONNECTION_STYLES[selectedEdge.data!.type] + const updatedEdge = { + ...selectedEdge, + data: selectedEdge.data, + markerEnd: { type: style.markerEnd, color: style.stroke }, + style: { stroke: style.stroke, strokeDasharray: style.strokeDasharray }, + animated: selectedEdge.data!.type === 'dependency', + } + + const updatedEdges = edges.map(e => e.id === selectedEdge.id ? updatedEdge : e) + setEdges(updatedEdges) + setSavedEdges(updatedEdges) + setEdgeDialogOpen(false) + toast.success('Connection updated!') } } @@ -351,19 +456,26 @@ export function FeatureIdeaCloud() { const result = JSON.parse(response) if (result.ideas && Array.isArray(result.ideas)) { - const newIdeas: FeatureIdea[] = result.ideas.map((idea: any, index: number) => ({ - id: `idea-ai-${Date.now()}-${index}`, + const newIdeas: FeatureIdea[] = result.ideas.map((idea: any) => ({ + id: `idea-ai-${Date.now()}-${Math.random()}`, title: idea.title, description: idea.description, category: idea.category || 'Other', priority: idea.priority || 'medium', status: 'idea' as const, createdAt: Date.now(), - x: 400 + (index * 250), - y: 300 + (index * 150), })) setIdeas((currentIdeas) => [...(currentIdeas || []), ...newIdeas]) + + const newNodes: Node[] = newIdeas.map((idea, index) => ({ + id: idea.id, + type: 'ideaNode', + position: { x: 400 + (index * 250), y: 300 + (index * 150) }, + data: idea, + })) + + setNodes((nds) => [...nds, ...newNodes]) toast.success(`Generated ${newIdeas.length} new ideas!`) } } catch (error) { @@ -372,625 +484,104 @@ export function FeatureIdeaCloud() { } } - const handleZoomIn = () => { - setZoom(z => Math.min(z * 1.2, 3)) - } - - const handleZoomOut = () => { - setZoom(z => Math.max(z / 1.2, 0.25)) - } - - const handleResetView = () => { - setZoom(1) - setPan({ x: 0, y: 0 }) - } - - const handleCanvasMouseDown = (e: React.MouseEvent) => { - if (tool === 'pan' || e.button === 1 || (e.button === 0 && e.ctrlKey)) { - setIsPanning(true) - setPanStart({ x: e.clientX - pan.x, y: e.clientY - pan.y }) - e.preventDefault() - } - } - - const handleCanvasMouseMove = (e: React.MouseEvent) => { - if (isPanning) { - setPan({ - x: e.clientX - panStart.x, - y: e.clientY - panStart.y, - }) - } else if (draggedIdea) { - const canvasX = (e.clientX - pan.x) / zoom - const canvasY = (e.clientY - pan.y) / zoom - - setIdeas((currentIdeas) => - (currentIdeas || []).map(idea => - idea.id === draggedIdea - ? { ...idea, x: canvasX - dragOffset.x, y: canvasY - dragOffset.y } - : idea - ) - ) - } else if (draggingConnection) { - setDraggingConnection({ - ...draggingConnection, - x: e.clientX, - y: e.clientY, - }) - } - } - - const handleCanvasMouseUp = (e: React.MouseEvent) => { - if (draggingConnection) { - setDraggingConnection(null) - } - setIsPanning(false) - setDraggedIdea(null) - } - - const handleIdeaMouseDown = (idea: FeatureIdea, e: React.MouseEvent) => { - if (tool === 'select') { - e.stopPropagation() - const canvasX = (e.clientX - pan.x) / zoom - const canvasY = (e.clientY - pan.y) / zoom - setDraggedIdea(idea.id) - setDragOffset({ - x: canvasX - idea.x, - y: canvasY - idea.y, - }) - } - } - - const handleWheel = (e: React.WheelEvent) => { - e.preventDefault() - const delta = e.deltaY > 0 ? 0.9 : 1.1 - const newZoom = Math.max(0.25, Math.min(3, zoom * delta)) - - const mouseX = e.clientX - const mouseY = e.clientY - - const zoomPointX = (mouseX - pan.x) / zoom - const zoomPointY = (mouseY - pan.y) / zoom - - const newPanX = mouseX - zoomPointX * newZoom - const newPanY = mouseY - zoomPointY * newZoom - - setZoom(newZoom) - setPan({ x: newPanX, y: newPanY }) - } - - const handleConnectionNodeMouseDown = (ideaId: string, e: React.MouseEvent) => { - e.stopPropagation() - const idea = safeIdeas.find(i => i.id === ideaId) - if (idea) { - setDraggingConnection({ - fromId: ideaId, - x: e.clientX, - y: e.clientY, - }) - } - } - - const handleConnectionNodeMouseUp = (ideaId: string, e: React.MouseEvent) => { - e.stopPropagation() - if (draggingConnection && draggingConnection.fromId !== ideaId) { - const existingConnection = safeConnections.find( - c => c.fromId === draggingConnection.fromId && c.toId === ideaId - ) - - if (existingConnection) { - toast.error('Connection already exists') - } else { - const newConnection: Connection = { - id: `conn-${Date.now()}`, - fromId: draggingConnection.fromId, - toId: ideaId, - type: connectionType, - label: CONNECTION_LABELS[connectionType], - } - setConnections((current) => [...(current || []), newConnection]) - toast.success('Ideas connected!') - } - } - setDraggingConnection(null) - } - - const renderArrowhead = (connection: Connection, x: number, y: number, angle: number) => { - const style = CONNECTION_STYLES[connection.type] - const size = 12 - - if (style.arrowType === 'open') { - const points = [ - [x, y], - [x - size * Math.cos(angle - Math.PI / 6), y - size * Math.sin(angle - Math.PI / 6)], - [x - size * Math.cos(angle + Math.PI / 6), y - size * Math.sin(angle + Math.PI / 6)] - ] - return ( - p.join(',')).join(' ')} - fill="none" - stroke={style.stroke} - strokeWidth={2} - /> - ) - } else if (style.arrowType === 'line') { - return ( - - ) - } else if (style.arrowType === 'hollow') { - const points = [ - [x, y], - [x - size * Math.cos(angle - Math.PI / 6), y - size * Math.sin(angle - Math.PI / 6)], - [x - size * 0.7 * Math.cos(angle), y - size * 0.7 * Math.sin(angle)], - [x - size * Math.cos(angle + Math.PI / 6), y - size * Math.sin(angle + Math.PI / 6)] - ] - return ( - p.join(',')).join(' ')} - fill="hsl(var(--background))" - stroke={style.stroke} - strokeWidth={2} - /> - ) - } else if (style.arrowType === 'diamond-filled') { - const points = [ - [x, y], - [x - size * 0.6 * Math.cos(angle - Math.PI / 3), y - size * 0.6 * Math.sin(angle - Math.PI / 3)], - [x - size * Math.cos(angle), y - size * Math.sin(angle)], - [x - size * 0.6 * Math.cos(angle + Math.PI / 3), y - size * 0.6 * Math.sin(angle + Math.PI / 3)] - ] - return ( - p.join(',')).join(' ')} - fill={style.stroke} - stroke={style.stroke} - strokeWidth={2} - /> - ) - } else if (style.arrowType === 'diamond-hollow') { - const points = [ - [x, y], - [x - size * 0.6 * Math.cos(angle - Math.PI / 3), y - size * 0.6 * Math.sin(angle - Math.PI / 3)], - [x - size * Math.cos(angle), y - size * Math.sin(angle)], - [x - size * 0.6 * Math.cos(angle + Math.PI / 3), y - size * 0.6 * Math.sin(angle + Math.PI / 3)] - ] - return ( - p.join(',')).join(' ')} - fill="hsl(var(--background))" - stroke={style.stroke} - strokeWidth={2} - /> - ) - } - return null - } - - const renderConnections = () => { - const elements: React.ReactNode[] = [] - - safeConnections.forEach((connection) => { - const fromIdea = safeIdeas.find(i => i.id === connection.fromId) - const toIdea = safeIdeas.find(i => i.id === connection.toId) - - if (fromIdea && toIdea) { - const fromX = fromIdea.x * zoom + pan.x + 240 - const fromY = fromIdea.y * zoom + pan.y + 80 - const toX = toIdea.x * zoom + pan.x - const toY = toIdea.y * zoom + pan.y + 80 - - const dx = toX - fromX - const dy = toY - fromY - const angle = Math.atan2(dy, dx) - const distance = Math.sqrt(dx * dx + dy * dy) - - const arrowSize = 12 - const endX = toX - arrowSize * Math.cos(angle) - const endY = toY - arrowSize * Math.sin(angle) - - const midX = (fromX + toX) / 2 - const midY = (fromY + toY) / 2 - - const style = CONNECTION_STYLES[connection.type] - const isHovered = hoveredConnection === connection.id - - elements.push( - - setHoveredConnection(connection.id)} - onMouseLeave={() => setHoveredConnection(null)} - onClick={(e) => handleConnectionClick(connection, e as any)} - /> - - setHoveredConnection(connection.id)} - onMouseLeave={() => setHoveredConnection(null)} - onClick={(e) => handleConnectionClick(connection, e as any)} - /> - - {renderArrowhead(connection, toX, toY, angle)} - - {(isHovered || connection.label) && ( - - - - {connection.label || CONNECTION_LABELS[connection.type]} - - - )} - - ) - } - }) - - if (draggingConnection) { - const fromIdea = safeIdeas.find(i => i.id === draggingConnection.fromId) - if (fromIdea) { - const fromX = fromIdea.x * zoom + pan.x + 240 - const fromY = fromIdea.y * zoom + pan.y + 80 - const toX = draggingConnection.x - const toY = draggingConnection.y - - const style = CONNECTION_STYLES[connectionType] - - elements.push( - - - - - ) - } - } - - return elements - } - return (
-
- - - - - - Select & Drag - - - - - - - Pan Canvas - - - - - - - Connect Ideas - - - -
- Type: - -
- -
- - - - - - - Zoom In - - - - - - - Zoom Out - - - - - - - Reset View - - - -
- {Math.round(zoom * 100)}% -
-
- -
- - - - - - AI Generate Ideas - - - - - - - Add Idea - - -
- -
-

Connection Types:

-
- {CONNECTION_TYPES.map(type => { - const style = CONNECTION_STYLES[type] - return ( -
- - - - {type} -
- ) - })} -
-
- -
-

💡 Tip: Double-click ideas to view details

-

🔗 Drag connection nodes on card sides to connect ideas

-

⚙️ Change connection type in toolbar before connecting

-
- -
- - {renderConnections()} - - -
- {safeIdeas.map((idea) => ( - handleIdeaMouseDown(idea, e)} - onClick={(e) => handleIdeaClick(idea, e)} - onDoubleClick={(e) => handleIdeaDoubleClick(idea, e)} + + + + +
+ Connection Type: + +
+
-
{ - e.stopPropagation() - handleConnectionNodeMouseDown(idea.id, e) - }} - onMouseUp={(e) => { - e.stopPropagation() - handleConnectionNodeMouseUp(idea.id, e) - }} - onMouseEnter={() => setHoveredNode(`${idea.id}-right`)} - onMouseLeave={() => setHoveredNode(null)} - > -
-
-
-
+ + + + + + + AI Generate Ideas + - -
-
-

{idea.title}

- -
-

- {idea.description} -

-
- - {idea.category} - - - {idea.status} - -
-
-
-
-
- ))} -
+ + + + + Add Idea + + + - {safeIdeas.length === 0 && ( -
-
-

No ideas yet

- + +
+

Connection Types:

+
+ {CONNECTION_TYPES.map(type => { + const style = CONNECTION_STYLES[type] + return ( +
+ + + + {type} +
+ ) + })}
- )} -
+ + + +
+

💡 Tip: Double-click ideas to view details

+

🔗 Drag from handles on card edges to connect ideas

+

⚙️ Click connections to edit or delete them

+
+
+ @@ -1135,23 +726,22 @@ export function FeatureIdeaCloud() {
- {safeConnections - .filter(c => c.fromId === selectedIdea.id || c.toId === selectedIdea.id) - .map(conn => { - const otherIdea = safeIdeas.find(i => - i.id === (conn.fromId === selectedIdea.id ? conn.toId : conn.fromId) - ) - const isOutgoing = conn.fromId === selectedIdea.id + {edges + .filter(e => e.source === selectedIdea.id || e.target === selectedIdea.id) + .map(edge => { + const otherIdeaId = edge.source === selectedIdea.id ? edge.target : edge.source + const otherIdea = safeIdeas.find(i => i.id === otherIdeaId) + const isOutgoing = edge.source === selectedIdea.id return ( -
- {conn.type} +
+ {edge.data?.type || 'unknown'} {isOutgoing ? '→' : '←'} {otherIdea?.title || 'Unknown'}
) })} - {safeConnections.filter(c => c.fromId === selectedIdea.id || c.toId === selectedIdea.id).length === 0 && ( + {edges.filter(e => e.source === selectedIdea.id || e.target === selectedIdea.id).length === 0 && (

No connections

)}
@@ -1165,7 +755,7 @@ export function FeatureIdeaCloud() { @@ -1173,7 +763,7 @@ export function FeatureIdeaCloud() {
- + Connection Details @@ -1182,19 +772,19 @@ export function FeatureIdeaCloud() { - {selectedConnection && ( + {selectedEdge && selectedEdge.data && (

- {safeIdeas.find(i => i.id === selectedConnection.fromId)?.title} + {safeIdeas.find(i => i.id === selectedEdge.source)?.title}

- {safeIdeas.find(i => i.id === selectedConnection.toId)?.title} + {safeIdeas.find(i => i.id === selectedEdge.target)?.title}

@@ -1202,11 +792,14 @@ export function FeatureIdeaCloud() {
setSelectedConnection({ - ...selectedConnection, - label: e.target.value + value={selectedEdge.data.label || ''} + onChange={(e) => setSelectedEdge({ + ...selectedEdge, + data: { + ...selectedEdge.data!, + label: e.target.value + } })} - placeholder={CONNECTION_LABELS[selectedConnection.type]} + placeholder={CONNECTION_LABELS[selectedEdge.data.type]} />
@@ -1247,26 +843,16 @@ export function FeatureIdeaCloud() {
- -