diff --git a/ui/package-lock.json b/ui/package-lock.json index ef966bffe57..843ca4d48d7 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -2483,9 +2483,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz", + "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==", "cpu": [ "ppc64" ], @@ -2500,9 +2500,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz", + "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==", "cpu": [ "arm" ], @@ -2517,9 +2517,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz", + "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==", "cpu": [ "arm64" ], @@ -2534,9 +2534,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz", + "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==", "cpu": [ "x64" ], @@ -2551,9 +2551,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz", + "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==", "cpu": [ "arm64" ], @@ -2568,9 +2568,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz", + "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==", "cpu": [ "x64" ], @@ -2585,9 +2585,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz", + "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==", "cpu": [ "arm64" ], @@ -2602,9 +2602,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz", + "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==", "cpu": [ "x64" ], @@ -2619,9 +2619,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz", + "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==", "cpu": [ "arm" ], @@ -2636,9 +2636,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz", + "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==", "cpu": [ "arm64" ], @@ -2653,9 +2653,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz", + "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==", "cpu": [ "ia32" ], @@ -2670,9 +2670,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz", + "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==", "cpu": [ "loong64" ], @@ -2687,9 +2687,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz", + "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==", "cpu": [ "mips64el" ], @@ -2704,9 +2704,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz", + "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==", "cpu": [ "ppc64" ], @@ -2721,9 +2721,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz", + "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==", "cpu": [ "riscv64" ], @@ -2738,9 +2738,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz", + "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==", "cpu": [ "s390x" ], @@ -2755,9 +2755,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz", + "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==", "cpu": [ "x64" ], @@ -2772,9 +2772,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz", + "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==", "cpu": [ "arm64" ], @@ -2789,9 +2789,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz", + "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==", "cpu": [ "x64" ], @@ -2806,9 +2806,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz", + "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==", "cpu": [ "arm64" ], @@ -2823,9 +2823,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz", + "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==", "cpu": [ "x64" ], @@ -2840,9 +2840,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz", + "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==", "cpu": [ "arm64" ], @@ -2857,9 +2857,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz", + "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==", "cpu": [ "x64" ], @@ -2874,9 +2874,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz", + "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==", "cpu": [ "arm64" ], @@ -2891,9 +2891,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz", + "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==", "cpu": [ "ia32" ], @@ -2908,9 +2908,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz", + "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==", "cpu": [ "x64" ], @@ -3621,19 +3621,6 @@ "msw": "^2.10.0" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "0.2.12", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", - "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "^1.4.3", - "@emnapi/runtime": "^1.4.3", - "@tybys/wasm-util": "^0.10.0" - } - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -10443,6 +10430,19 @@ "node": ">=14.0.0" } }, + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", @@ -12219,9 +12219,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.221", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.221.tgz", - "integrity": "sha512-/1hFJ39wkW01ogqSyYoA4goOXOtMRy6B+yvA1u42nnsEGtHzIzmk93aPISumVQeblj47JUHLC9coCjUxb1EvtQ==", + "version": "1.5.220", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.220.tgz", + "integrity": "sha512-TWXijEwR1ggr4BdAKrb1nMNqYLTx1/4aD1fkeZU+FVJGTKu53/T7UyHKXlqEX3Ub02csyHePbHmkvnrjcaYzMA==", "dev": true, "license": "ISC" }, @@ -12460,9 +12460,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz", + "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12473,32 +12473,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.9", + "@esbuild/android-arm": "0.25.9", + "@esbuild/android-arm64": "0.25.9", + "@esbuild/android-x64": "0.25.9", + "@esbuild/darwin-arm64": "0.25.9", + "@esbuild/darwin-x64": "0.25.9", + "@esbuild/freebsd-arm64": "0.25.9", + "@esbuild/freebsd-x64": "0.25.9", + "@esbuild/linux-arm": "0.25.9", + "@esbuild/linux-arm64": "0.25.9", + "@esbuild/linux-ia32": "0.25.9", + "@esbuild/linux-loong64": "0.25.9", + "@esbuild/linux-mips64el": "0.25.9", + "@esbuild/linux-ppc64": "0.25.9", + "@esbuild/linux-riscv64": "0.25.9", + "@esbuild/linux-s390x": "0.25.9", + "@esbuild/linux-x64": "0.25.9", + "@esbuild/netbsd-arm64": "0.25.9", + "@esbuild/netbsd-x64": "0.25.9", + "@esbuild/openbsd-arm64": "0.25.9", + "@esbuild/openbsd-x64": "0.25.9", + "@esbuild/openharmony-arm64": "0.25.9", + "@esbuild/sunos-x64": "0.25.9", + "@esbuild/win32-arm64": "0.25.9", + "@esbuild/win32-ia32": "0.25.9", + "@esbuild/win32-x64": "0.25.9" } }, "node_modules/escalade": { @@ -19188,26 +19188,15 @@ } }, "packages/smart-tools/node_modules/@types/node": { - "version": "22.18.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.6.tgz", - "integrity": "sha512-r8uszLPpeIWbNKtvWRt/DbVi5zbqZyj1PTmhRMqBMvDnaz1QpmSKujUtJLrqGZeoM8v72MfYggDceY4K1itzWQ==", + "version": "22.18.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.5.tgz", + "integrity": "sha512-g9BpPfJvxYBXUWI9bV37j6d6LTMNQ88hPwdWWUeYZnMhlo66FIg9gCc1/DZb15QylJSKwOZjwrckvOTWpOiChg==", "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, - "packages/smart-tools/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "packages/smart-tools/node_modules/eslint": { "version": "9.35.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz", @@ -19326,42 +19315,6 @@ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, - "packages/smart-tools/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "packages/smart-tools/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "packages/smart-tools/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, "packages/smart-tools/node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", diff --git a/ui/rsbuild.config.ts b/ui/rsbuild.config.ts index b4db27faa5c..8dcb0465315 100644 --- a/ui/rsbuild.config.ts +++ b/ui/rsbuild.config.ts @@ -29,7 +29,6 @@ export default defineConfig({ }, }), ], - source: { define: { ...publicVars, @@ -54,4 +53,17 @@ export default defineConfig({ }, }, }, + server: { + headers: { + 'Cross-Origin-Embedder-Policy': 'credentialless', + 'Cross-Origin-Opener-Policy': 'same-origin', + 'Content-Security-Policy': + "default-src 'self'; " + + "script-src 'self' 'unsafe-eval' blob:; " + + "worker-src 'self' blob:; " + + "connect-src 'self' http://localhost:7860 data:; " + + "img-src 'self' http://localhost:7860 data: blob:; " + + "style-src 'self' 'unsafe-inline';", + }, + }, }); diff --git a/ui/src/features/annotator/annotations/annotations.component.tsx b/ui/src/features/annotator/annotations/annotations.component.tsx index a6a7b07af0a..ee60cf17665 100644 --- a/ui/src/features/annotator/annotations/annotations.component.tsx +++ b/ui/src/features/annotator/annotations/annotations.component.tsx @@ -1,6 +1,10 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 +import { CSSProperties } from 'react'; + +import { isEmpty } from 'lodash-es'; + import { useAnnotator } from '../annotator-provider.component'; import { useSelectedAnnotations } from '../select-annotation-provider.component'; import { Annotation } from './annotation.component'; @@ -12,6 +16,17 @@ type AnnotationsProps = { isFocussed: boolean; }; +const DEFAULT_ANNOTATION_STYLES = { + fillOpacity: 0.4, + fill: 'var(--annotation-fill)', + stroke: 'var(--annotation-stroke)', + strokeLinecap: 'round', + strokeWidth: 'calc(1px / var(--zoom-scale))', + strokeDashoffset: 0, + strokeDasharray: 0, + strokeOpacity: 'var(--annotation-border-opacity, 1)', +} satisfies CSSProperties; + export const Annotations = ({ width, height, isFocussed }: AnnotationsProps) => { const { annotations } = useAnnotator(); const { selectedAnnotations } = useSelectedAnnotations(); @@ -22,11 +37,17 @@ export const Annotations = ({ width, height, isFocussed }: AnnotationsProps) => ...annotations.filter((a) => selectedAnnotations.has(a.id)), ]; + if (isEmpty(annotations)) { + return <>; + } + return ( - - {orderedAnnotations.map((annotation) => ( - - ))} - + + + {orderedAnnotations.map((annotation) => ( + + ))} + + ); }; diff --git a/ui/src/features/annotator/annotator-canvas.tsx b/ui/src/features/annotator/annotator-canvas.tsx index 16c057cae69..da9008c81b0 100644 --- a/ui/src/features/annotator/annotator-canvas.tsx +++ b/ui/src/features/annotator/annotator-canvas.tsx @@ -1,10 +1,9 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { CSSProperties, MouseEvent } from 'react'; +import { PointerEvent } from 'react'; import { Grid, View } from '@geti/ui'; -import { isEmpty } from 'lodash-es'; import { ZoomProvider } from '../../components/zoom/zoom'; import { ZoomTransform } from '../../components/zoom/zoom-transform'; @@ -15,29 +14,20 @@ import { useSelectedAnnotations } from './select-annotation-provider.component'; import { ToolManager } from './tools/tool-manager.component'; import { Annotation, DatasetItem } from './types'; -const DEFAULT_ANNOTATION_STYLES = { - fillOpacity: 0.4, - fill: 'var(--annotation-fill)', - stroke: 'var(--annotation-stroke)', - strokeLinecap: 'round', - strokeWidth: 'calc(1px / var(--zoom-scale))', - strokeDashoffset: 0, - strokeDasharray: 0, - strokeOpacity: 'var(--annotation-border-opacity, 1)', -} satisfies CSSProperties; - type AnnotatorCanvasProps = { mediaItem: DatasetItem; isFocussed: boolean; }; export const AnnotatorCanvas = ({ mediaItem, isFocussed }: AnnotatorCanvasProps) => { - const { setSelectedAnnotations } = useSelectedAnnotations(); const project_id = useProjectIdentifier(); + const { setSelectedAnnotations } = useSelectedAnnotations(); + const size = { width: mediaItem.width, height: mediaItem.height }; - // todo: pass media annotations + // TODO: pass media annotations + // eslint-disable-next-line @typescript-eslint/no-unused-vars const annotations: Annotation[] = []; - const handleClickOutside = (e: MouseEvent): void => { + const handleClickOutside = (e: PointerEvent): void => { if (e.target === e.currentTarget) { setSelectedAnnotations(new Set()); } @@ -51,20 +41,14 @@ export const AnnotatorCanvas = ({ mediaItem, isFocussed }: AnnotatorCanvasProps) Collected data - {!isEmpty(annotations) && ( - - + + <> +
- - - - - )} +
+ + +
diff --git a/ui/src/features/annotator/annotator-provider.component.tsx b/ui/src/features/annotator/annotator-provider.component.tsx index f0b514a76d2..48b81fc361e 100644 --- a/ui/src/features/annotator/annotator-provider.component.tsx +++ b/ui/src/features/annotator/annotator-provider.component.tsx @@ -6,17 +6,23 @@ import { createContext, Dispatch, ReactNode, SetStateAction, useContext, useStat import { v4 as uuid } from 'uuid'; import { ToolType } from '../../components/tool-selection-bar/tools/interface'; -import { Annotation, DatasetItem, Shape } from './types'; +import { useLoadImageQuery } from './hooks/use-load-image-query.hook'; +import { Annotation, DatasetItem, RegionOfInterest, Shape } from './types'; type AnnotatorContext = { + // Tools activeTool: ToolType | null; setActiveTool: Dispatch>; + // Annotations + annotations: Annotation[]; addAnnotation: (shape: Shape) => void; updateAnnotation: (updatedAnnotation: Annotation) => void; + // Media item mediaItem: DatasetItem; - annotations: Annotation[]; + image: ImageData; + roi: RegionOfInterest; }; export const AnnotatorProviderContext = createContext(null); @@ -26,6 +32,8 @@ export const AnnotatorProvider = ({ mediaItem, children }: { mediaItem: DatasetI // todo: pass media annotations const [annotations, setAnnotations] = useState([]); + const imageQuery = useLoadImageQuery(mediaItem); + const updateAnnotation = (updatedAnnotation: Annotation) => { const { id } = updatedAnnotation; @@ -57,6 +65,8 @@ export const AnnotatorProvider = ({ mediaItem, children }: { mediaItem: DatasetI annotations, mediaItem, + image: imageQuery.data, + roi: { x: 0, y: 0, width: mediaItem.width, height: mediaItem.height }, }} > {children} diff --git a/ui/src/features/annotator/hooks/use-load-image-query.hook.ts b/ui/src/features/annotator/hooks/use-load-image-query.hook.ts new file mode 100644 index 00000000000..cf8ef4d133a --- /dev/null +++ b/ui/src/features/annotator/hooks/use-load-image-query.hook.ts @@ -0,0 +1,30 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { useSuspenseQuery, UseSuspenseQueryResult } from '@tanstack/react-query'; + +import { API_BASE_URL } from '../../../api/client'; +import { useProjectIdentifier } from '../../../hooks/use-project-identifier.hook'; +import { getImageData, loadImage } from '../tools/utils'; +import { DatasetItem } from '../types'; + +export const useLoadImageQuery = (mediaItem: DatasetItem | undefined): UseSuspenseQueryResult => { + const projectId = useProjectIdentifier(); + + return useSuspenseQuery({ + queryKey: ['mediaItem', mediaItem?.id, projectId], + queryFn: async () => { + if (mediaItem === undefined) { + throw new Error("Can't fetch undefined media item"); + } + + const imageUrl = `${API_BASE_URL}/api/projects/${projectId}/dataset/items/${mediaItem.id}/binary`; + const image = await loadImage(imageUrl); + + return getImageData(image); + }, + // The image of a media item never changes so we don't want to refetch stale data + staleTime: Infinity, + retry: 0, + }); +}; diff --git a/ui/src/features/annotator/hooks/use-segment-anything.hook.ts b/ui/src/features/annotator/hooks/use-segment-anything.hook.ts deleted file mode 100644 index 94c79ad3bee..00000000000 --- a/ui/src/features/annotator/hooks/use-segment-anything.hook.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright (C) 2025 Intel Corporation -// SPDX-License-Identifier: Apache-2.0 - -import { useQuery } from '@tanstack/react-query'; -import { wrap } from 'comlink'; - -export const useSegmentAnythingWorkerQuery = () => { - return useQuery({ - queryKey: ['workers', 'segment-anything'], - queryFn: async () => { - const segmentAnythingWorker = wrap( - new Worker(new URL('../webworkers/segment-anything.worker', import.meta.url), { - type: 'module', - }) - ); - // @ts-expect-error build exists on every worker - return segmentAnythingWorker.build(); - }, - staleTime: Infinity, - }); -}; diff --git a/ui/src/features/annotator/loading.component.tsx b/ui/src/features/annotator/loading.component.tsx new file mode 100644 index 00000000000..6b15cbc657e --- /dev/null +++ b/ui/src/features/annotator/loading.component.tsx @@ -0,0 +1,44 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { Flex, Heading, View } from '@geti/ui'; + +import IntelBrandedLoadingGif from '../../assets/intel-loading.webp'; + +export const AnnotatorLoading = ({ isLoading }: { isLoading: boolean }) => { + return ( + + + {/* eslint-disable-next-line jsx-a11y/img-redundant-alt */} + Processing image + + {isLoading && 'Processing image, please wait...'} + + + + ); +}; diff --git a/ui/src/features/annotator/tools/annotator-tools.component.tsx b/ui/src/features/annotator/tools/annotator-tools.component.tsx index 68bc122f8bd..ffe19ff898b 100644 --- a/ui/src/features/annotator/tools/annotator-tools.component.tsx +++ b/ui/src/features/annotator/tools/annotator-tools.component.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Divider } from '@geti/ui'; -import { BoundingBox, Selector } from '@geti/ui/icons'; +import { BoundingBox, SegmentAnythingIcon, Selector } from '@geti/ui/icons'; import { ToolConfig } from '../../../components/tool-selection-bar/tools/interface'; import { Tools } from '../../../components/tool-selection-bar/tools/tools.component'; @@ -17,7 +17,8 @@ const TASK_TOOL_CONFIG: Record = { ], segmentation: [ { type: 'selection', icon: Selector }, - // TODO: Add 'polygon' and 'sam' tools later + { type: 'sam', icon: SegmentAnythingIcon }, + // TODO: Add 'polygon' tool later ], }; diff --git a/ui/src/features/annotator/tools/bounding-box-tool/bounding-box-tool.component.tsx b/ui/src/features/annotator/tools/bounding-box-tool/bounding-box-tool.component.tsx index c90cc9a5cf0..793c2e87a34 100644 --- a/ui/src/features/annotator/tools/bounding-box-tool/bounding-box-tool.component.tsx +++ b/ui/src/features/annotator/tools/bounding-box-tool/bounding-box-tool.component.tsx @@ -6,13 +6,13 @@ import { useAnnotator } from '../../annotator-provider.component'; import { DrawingBox } from '../drawing-box-tool/drawing-box.component'; export const BoundingBoxTool = () => { - const { mediaItem, addAnnotation } = useAnnotator(); + const { mediaItem, addAnnotation, image } = useAnnotator(); const { scale: zoom } = useZoom(); return ( diff --git a/ui/src/features/annotator/tools/drawing-box-tool/crosshair/crosshair-line.component.tsx b/ui/src/features/annotator/tools/drawing-box-tool/crosshair/crosshair-line.component.tsx index 20e84557035..b5170559693 100644 --- a/ui/src/features/annotator/tools/drawing-box-tool/crosshair/crosshair-line.component.tsx +++ b/ui/src/features/annotator/tools/drawing-box-tool/crosshair/crosshair-line.component.tsx @@ -23,7 +23,7 @@ const colors = { }; export const CrosshairLine = ({ direction, point }: CrosshairLineProps) => { - const sizeRatio = `calc(${DEFAULT_SIZE} / var(--zoom-level))`; + const sizeRatio = `calc(${DEFAULT_SIZE} / var(--zoom-scale))`; const attributes = direction === 'horizontal' ? { diff --git a/ui/src/features/annotator/tools/segment-anything-tool/segment-anything-tool.component.tsx b/ui/src/features/annotator/tools/segment-anything-tool/segment-anything-tool.component.tsx new file mode 100644 index 00000000000..c34504166d2 --- /dev/null +++ b/ui/src/features/annotator/tools/segment-anything-tool/segment-anything-tool.component.tsx @@ -0,0 +1,150 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { PointerEvent, useEffect, useRef, useState } from 'react'; + +import { clampPointBetweenImage } from '@geti/smart-tools/utils'; + +import { useZoom } from '../../../../components/zoom/zoom'; +import { AnnotationShape } from '../../annotations/annotation-shape.component'; +import { MaskAnnotations } from '../../annotations/mask-annotations.component'; +import { useAnnotator } from '../../annotator-provider.component'; +import { AnnotatorLoading } from '../../loading.component'; +import { Annotation, Shape } from '../../types'; +import { SvgToolCanvas } from '../svg-tool-canvas.component'; +import { getRelativePoint, removeOffLimitPoints } from '../utils'; +import { InteractiveAnnotationPoint } from './segment-anything.interface'; +import { useDecodingMutation } from './use-decoding-query.hook'; +import { useSegmentAnythingModel } from './use-segment-anything.hook'; +import { useSingleStackFn } from './use-single-stack-fn.hook'; +import { useThrottledCallback } from './use-throttle-callback.hook'; + +import classes from './segment-anything.module.scss'; + +// Whenever the user moves their mouse over the canvas we compute a preview of +// SAM being applied to the user's mouse position. +// The decoding step of SAM takes on average 100ms with 150-250ms being a high +// exception. We throttle the mouse update based on this so that we don't overload +// the user's cpu with too many decoding requests +const THROTTLE_TIME = 150; + +const SELECT_ANNOTATION_STYLES = { + fillOpacity: 0.3, + fill: 'var(--energy-blue-shade)', + stroke: 'var(--energy-blue-shade)', + strokeWidth: 'calc(2px / var(--zoom-scale))', +}; + +export const SegmentAnythingTool = () => { + const [mousePosition, setMousePosition] = useState(); + const [previewShapes, setPreviewShapes] = useState([]); + + const zoom = useZoom(); + const { mediaItem, roi, image } = useAnnotator(); + const { isLoading, decodingQueryFn } = useSegmentAnythingModel(); + const throttledDecodingQueryFn = useSingleStackFn(decodingQueryFn); + const decodingMutation = useDecodingMutation(decodingQueryFn); + + const ref = useRef(null); + + const clampPoint = clampPointBetweenImage(image); + + const throttleSetMousePosition = useThrottledCallback((point: InteractiveAnnotationPoint) => { + setMousePosition(point); + }, THROTTLE_TIME); + + useEffect(() => { + if (mousePosition === undefined) { + return; + } + + throttledDecodingQueryFn([mousePosition]) + .then((shapes) => { + setPreviewShapes(shapes.map((shape) => removeOffLimitPoints(shape, roi))); + + throttleSetMousePosition.flush(); + }) + .catch(() => { + // If getting decoding went wrong we set an empty preview and + // start to compute the next decoding + return []; + }); + }, [mousePosition, throttledDecodingQueryFn, throttleSetMousePosition, roi]); + + const handleMouseMove = (event: PointerEvent) => { + if (!ref.current) { + return; + } + + const point = clampPoint(getRelativePoint(ref.current, { x: event.clientX, y: event.clientY }, zoom.scale)); + + throttleSetMousePosition({ ...point, positive: true }); + }; + + const onPointerUp = (event: PointerEvent) => { + if (!ref.current) { + return; + } + + if (event.button !== 0 && event.button !== 2) { + return; + } + + const point = clampPoint(getRelativePoint(ref.current, { x: event.clientX, y: event.clientY }, zoom.scale)); + + decodingMutation.mutate([{ ...point, positive: true }]); + }; + + const annotations = previewShapes.map((shape, idx): Annotation => { + return { + shape, + labels: [{ id: 'id', color: 'red', name: 'Segment Anything', isPrediction: false }], + id: `${idx}`, + }; + }); + + if (isLoading) { + return ; + } + + return ( + { + throttleSetMousePosition.cancel(); + setMousePosition(undefined); + setPreviewShapes([]); + }} + style={{ + cursor: `url("/icons/selection.svg") 8 8, auto`, + }} + > + + <> + + + {previewShapes.length > 0 && + previewShapes.map((shape, idx) => ( + + + + ))} + + ); +}; diff --git a/ui/src/features/annotator/tools/segment-anything-tool/segment-anything.interface.ts b/ui/src/features/annotator/tools/segment-anything-tool/segment-anything.interface.ts new file mode 100644 index 00000000000..97515742d73 --- /dev/null +++ b/ui/src/features/annotator/tools/segment-anything-tool/segment-anything.interface.ts @@ -0,0 +1,8 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { Point } from '../../types'; + +export interface InteractiveAnnotationPoint extends Point { + positive: boolean; +} diff --git a/ui/src/features/annotator/tools/segment-anything-tool/segment-anything.module.scss b/ui/src/features/annotator/tools/segment-anything-tool/segment-anything.module.scss new file mode 100644 index 00000000000..b409528f6e5 --- /dev/null +++ b/ui/src/features/annotator/tools/segment-anything-tool/segment-anything.module.scss @@ -0,0 +1,24 @@ +.stroke { + stroke-dasharray: 10; + stroke-dashoffset: 10; +} + +.animateStroke { + stroke-dasharray: 10; + stroke-dashoffset: 10; + animation: dash 40s linear infinite; +} + +@keyframes dash { + to { + stroke-dashoffset: 1000; + } +} + +.layer { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; +} diff --git a/ui/src/features/annotator/tools/segment-anything-tool/use-decoding-query.hook.ts b/ui/src/features/annotator/tools/segment-anything-tool/use-decoding-query.hook.ts new file mode 100644 index 00000000000..c385ab5019e --- /dev/null +++ b/ui/src/features/annotator/tools/segment-anything-tool/use-decoding-query.hook.ts @@ -0,0 +1,65 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { queryOptions, useMutation, useQuery } from '@tanstack/react-query'; + +import { useAnnotator } from '../../annotator-provider.component'; +import { Shape } from '../../types'; +import { removeOffLimitPoints } from '../utils'; +import { InteractiveAnnotationPoint } from './segment-anything.interface'; + +const roundPoint = (point: InteractiveAnnotationPoint): InteractiveAnnotationPoint => ({ + x: Math.round(point.x), + y: Math.round(point.y), + positive: point.positive, +}); + +export const useDecodingQueryOptions = ( + points: InteractiveAnnotationPoint[], + queryFn: (points: InteractiveAnnotationPoint[]) => Promise +) => { + const { mediaItem, roi } = useAnnotator(); + // Round points so that when the user slightly moves their mouse we do not + // immediately recompute the decoding + const roundedPoints = points.map(roundPoint); + + return queryOptions({ + queryKey: ['segment-anything-model', 'decoding', mediaItem?.id, roundedPoints, roi], + queryFn: async () => { + const shapes = await queryFn(roundedPoints); + + return shapes.map((shape) => { + return removeOffLimitPoints(shape, roi); + }); + }, + staleTime: Infinity, + retry: 0, + }); +}; + +export const useDecodingQuery = ( + points: InteractiveAnnotationPoint[], + queryFn: (points: InteractiveAnnotationPoint[]) => Promise +) => { + const decodingQueryOptions = useDecodingQueryOptions(points, queryFn); + + return useQuery(decodingQueryOptions); +}; + +export const useDecodingMutation = (queryFn: (points: InteractiveAnnotationPoint[]) => Promise) => { + const { addAnnotation, roi } = useAnnotator(); + + return useMutation({ + mutationFn: async (points: InteractiveAnnotationPoint[]) => { + // Round points so that when the user slightly moves their mouse we do not + // immediately recompute the decoding + const roundedPoints = points.map(roundPoint); + + const shapes = (await queryFn(roundedPoints)).map((shape) => { + return removeOffLimitPoints(shape, roi); + }); + + shapes.map((shape) => addAnnotation(shape)); + }, + }); +}; diff --git a/ui/src/features/annotator/tools/segment-anything-tool/use-segment-anything.hook.ts b/ui/src/features/annotator/tools/segment-anything-tool/use-segment-anything.hook.ts new file mode 100644 index 00000000000..0d3c196cd16 --- /dev/null +++ b/ui/src/features/annotator/tools/segment-anything-tool/use-segment-anything.hook.ts @@ -0,0 +1,127 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useRef, useState } from 'react'; + +import { EncodingOutput, SegmentAnythingModel } from '@geti/smart-tools/segment-anything'; +import { useQuery } from '@tanstack/react-query'; +import { Remote, wrap } from 'comlink'; + +import { useAnnotator } from '../../annotator-provider.component'; +import { DatasetItem } from '../../types'; +import { convertToolShapeToGetiShape } from '../utils'; +import { InteractiveAnnotationPoint } from './segment-anything.interface'; + +const useSegmentAnythingWorker = (algorithmType: 'SEGMENT_ANYTHING_DECODER' | 'SEGMENT_ANYTHING_ENCODER') => { + const { data } = useQuery>({ + queryKey: ['workers', algorithmType], + queryFn: async () => { + const baseWorker = new Worker(new URL('../../webworkers/segment-anything.worker', import.meta.url), { + type: 'module', + }); + const samWorker = wrap(baseWorker); + + // @ts-expect-error build exists on every worker + return samWorker.build(); + }, + staleTime: Infinity, + }); + + const modelRef = useRef>(undefined); + const [modelIsLoading, setModelIsLoading] = useState(false); + + useEffect(() => { + const loadWorker = async () => { + setModelIsLoading(true); + + if (data) { + const model = data; + + await model.init(algorithmType); + + modelRef.current = model; + } + + setModelIsLoading(false); + }; + + if (data && modelRef.current === undefined && !modelIsLoading) { + loadWorker(); + } + }, [data, modelIsLoading, algorithmType]); + + return modelRef.current; +}; + +const useEncodingQuery = ( + model: Remote | undefined, + mediaItem: DatasetItem, + image: ImageData +) => { + return useQuery({ + queryKey: ['segment-anything-model', 'encoding', mediaItem?.id], + queryFn: async () => { + if (model === undefined) { + throw new Error('Model not yet initialized'); + } + + if (image === undefined) { + throw new Error('Image not available'); + } + + return await model.processEncoder(image); + }, + staleTime: Infinity, + gcTime: 3600 * 15, + enabled: model !== undefined && mediaItem !== undefined, + }); +}; + +const useDecodingFn = (model: Remote | undefined, encoding: EncodingOutput | undefined) => { + // TODO: look into returning a new "decoder model" instance that already has the encoding data + // stored in memory, to reduce memory usage + return async (points: InteractiveAnnotationPoint[]) => { + if (points.length === 0) { + return []; + } + + if (model === undefined) { + return []; + } + + if (encoding === undefined) { + return []; + } + + const { shapes } = await model.processDecoder(encoding, { + points, + boxes: [], + ouputConfig: { + type: 'polygon', + }, + image: undefined, + }); + + return shapes.map(convertToolShapeToGetiShape); + }; +}; + +export const useSegmentAnythingModel = () => { + const encoderModel = useSegmentAnythingWorker('SEGMENT_ANYTHING_ENCODER'); + const decoderModel = useSegmentAnythingWorker('SEGMENT_ANYTHING_DECODER'); + const isLoadingWorkers = encoderModel === undefined || decoderModel === undefined; + + const { mediaItem, image } = useAnnotator(); + const encodingQuery = useEncodingQuery(encoderModel, mediaItem, image); + const decodingQueryFn = useDecodingFn(decoderModel, encodingQuery.data); + + const isLoading = isLoadingWorkers || encodingQuery.isLoading; + const isProcessing = encodingQuery.isFetching; + + return { + isLoading, + isProcessing, + encodingQuery, + decodingQueryFn, + }; +}; diff --git a/ui/src/features/annotator/tools/segment-anything-tool/use-single-stack-fn.hook.ts b/ui/src/features/annotator/tools/segment-anything-tool/use-single-stack-fn.hook.ts new file mode 100644 index 00000000000..bcd312c35ce --- /dev/null +++ b/ui/src/features/annotator/tools/segment-anything-tool/use-single-stack-fn.hook.ts @@ -0,0 +1,65 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { useCallback, useRef } from 'react'; + +export const useSingleStackFn = < + Callback extends (...args: Parameters) => Promise>>, +>( + fn: Callback +) => { + const resolveRef = useRef<() => void>(undefined); + const rejectRef = useRef<() => void>(undefined); + const isProcessing = useRef(false); + + const wrappedFn = useCallback( + async (...args: Parameters): Promise>> => { + // Wait for the previous function call to be finished + await new Promise(async (resolve, reject) => { + // Continue on if we are not waiting for the result of a previous invokation + if (!isProcessing.current) { + return resolve(); + } + + // If the function was invoked while waiting for the previous result then + // we reject the previous invocation + if (rejectRef.current) { + rejectRef.current(); + rejectRef.current = undefined; + resolveRef.current = undefined; + } + + // Let the previous invocation resolve this call, or let any subsequent calls + // cancel this call + rejectRef.current = reject; + resolveRef.current = resolve; + }); + + try { + isProcessing.current = true; + const result = await fn(...args); + return result; + } catch (error) { + // Reject subsequent invocations as something unexpected made the current invocation fail + if (rejectRef.current) { + rejectRef.current(); + rejectRef.current = undefined; + resolveRef.current = undefined; + } + throw error; + } finally { + isProcessing.current = false; + + // Resolve any subsequent invocations that were waiting for this function to complete + if (resolveRef.current) { + resolveRef.current(); + rejectRef.current = undefined; + resolveRef.current = undefined; + } + } + }, + [fn] + ); + + return wrappedFn; +}; diff --git a/ui/src/features/annotator/tools/segment-anything-tool/use-throttle-callback.hook.ts b/ui/src/features/annotator/tools/segment-anything-tool/use-throttle-callback.hook.ts new file mode 100644 index 00000000000..418d62b7a6a --- /dev/null +++ b/ui/src/features/annotator/tools/segment-anything-tool/use-throttle-callback.hook.ts @@ -0,0 +1,26 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { useEffect, useMemo } from 'react'; + +import { throttle, type DebouncedFunc } from 'lodash-es'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Callback = (...args: any[]) => void; + +export const useThrottledCallback = (callback: Callback, delay: number): DebouncedFunc => { + const debouncedCallback = useMemo(() => { + return throttle(callback, delay, { + leading: true, + trailing: true, + }); + }, [callback, delay]); + + useEffect(() => { + return () => { + debouncedCallback.cancel(); + }; + }, [debouncedCallback]); + + return debouncedCallback; +}; diff --git a/ui/src/features/annotator/tools/svg-tool-canvas.component.tsx b/ui/src/features/annotator/tools/svg-tool-canvas.component.tsx new file mode 100644 index 00000000000..551aee820bc --- /dev/null +++ b/ui/src/features/annotator/tools/svg-tool-canvas.component.tsx @@ -0,0 +1,35 @@ +// Copyright (C) 2025 Intel Corporation +// SPDX-License-Identifier: Apache-2.0 + +import { FC, PropsWithChildren, RefObject, SVGProps } from 'react'; + +import { roiFromImage } from '@geti/smart-tools/utils'; + +import { allowPanning } from '../utils'; + +type CanvasProps = SVGProps & { image: ImageData } & { canvasRef?: RefObject }; +// This svg component is used to by tools that need to add local listeners that work in +// a given region of interest. +// An invisible rect is rendered to guarantee that the svg gets a width and height. +export const SvgToolCanvas: FC> = ({ + image, + children, + canvasRef, + onPointerDown, + ...props +}) => { + const roi = roiFromImage(image); + + return ( + + + {children} + + ); +}; diff --git a/ui/src/features/annotator/tools/tool-manager.component.tsx b/ui/src/features/annotator/tools/tool-manager.component.tsx index 0254f5dfc94..9af3a71cbd3 100644 --- a/ui/src/features/annotator/tools/tool-manager.component.tsx +++ b/ui/src/features/annotator/tools/tool-manager.component.tsx @@ -3,12 +3,15 @@ import { useAnnotator } from '../annotator-provider.component'; import { BoundingBoxTool } from './bounding-box-tool/bounding-box-tool.component'; +import { SegmentAnythingTool } from './segment-anything-tool/segment-anything-tool.component'; export const ToolManager = () => { const { activeTool } = useAnnotator(); if (activeTool === 'bounding-box') { return ; + } else if (activeTool === 'sam') { + return ; } return null; diff --git a/ui/src/features/annotator/tools/utils.ts b/ui/src/features/annotator/tools/utils.ts index 0afb531bb64..ce3c755e13a 100644 --- a/ui/src/features/annotator/tools/utils.ts +++ b/ui/src/features/annotator/tools/utils.ts @@ -339,3 +339,54 @@ export const getRelativePoint = (element: ElementType, point: Point, zoom: numbe y: Math.round((point.y - rect.top) / zoom), }; }; + +export const loadImage = (link: string): Promise => + new Promise((resolve, reject) => { + const image = new Image(); + image.crossOrigin = 'anonymous'; + + image.onload = () => resolve(image); + image.onerror = (error) => reject(error); + + image.fetchPriority = 'high'; + image.src = link; + + if (process.env.NODE_ENV === 'test') { + // Immediately load the media item's image + resolve(image); + } + }); + +const drawImageOnCanvas = (img: HTMLImageElement, filter = ''): HTMLCanvasElement => { + const canvas: HTMLCanvasElement = document.createElement('canvas'); + + canvas.width = img.naturalWidth ? img.naturalWidth : img.width; + canvas.height = img.naturalHeight ? img.naturalHeight : img.height; + + const ctx = canvas.getContext('2d'); + + if (ctx) { + const width = img.naturalWidth ? img.naturalWidth : img.width; + const height = img.naturalHeight ? img.naturalHeight : img.height; + + ctx.filter = filter; + ctx.drawImage(img, 0, 0, width, height); + } + + return canvas; +}; + +export const getImageData = (img: HTMLImageElement): ImageData => { + // Always return valid imageData, even if the image isn't loaded yet. + if (img.width === 0 && img.height === 0) { + return new ImageData(1, 1); + } + + const canvas = drawImageOnCanvas(img); + const ctx = canvas.getContext('2d') as CanvasRenderingContext2D; + + const width = img.naturalWidth ? img.naturalWidth : img.width; + const height = img.naturalHeight ? img.naturalHeight : img.height; + + return ctx.getImageData(0, 0, width, height); +}; diff --git a/ui/src/features/dataset/media-preview/media-preview.component.tsx b/ui/src/features/dataset/media-preview/media-preview.component.tsx index d10f7246ac2..44033edfb38 100644 --- a/ui/src/features/dataset/media-preview/media-preview.component.tsx +++ b/ui/src/features/dataset/media-preview/media-preview.component.tsx @@ -1,7 +1,7 @@ // Copyright (C) 2025 Intel Corporation // SPDX-License-Identifier: Apache-2.0 -import { useState } from 'react'; +import { Suspense, useState } from 'react'; import { Button, ButtonGroup, Content, Dialog, Divider, Grid, Heading, ToggleButton, View } from '@geti/ui'; @@ -35,37 +35,39 @@ export const MediaPreview = ({ mediaItem, close }: MediaPreviewProps) => { columns={'100px calc(100% - 200px) 100px'} rows={'auto 1fr auto'} > - - - - + Loading...}> + + + + - - - - - + + + + + - -
Aside
-
+ +
Aside
+
- - - - Focus - - - - -
+ + + + Focus + + + + +
+