diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5f768ce..bdd7021 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,9 @@ "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.84.1", "@tanstack/react-router": "1.19.1", + "@tensorflow-models/coco-ssd": "^2.2.2", + "@tensorflow-models/deeplab": "^0.2.2", + "@tensorflow/tfjs": "^3.21.0", "@types/leaflet": "^1.9.20", "@types/react-grid-layout": "^1.3.5", "@types/uuid": "^10.0.0", @@ -2552,6 +2555,169 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tensorflow-models/coco-ssd": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@tensorflow-models/coco-ssd/-/coco-ssd-2.2.2.tgz", + "integrity": "sha512-Jey2JscmKEValcFZH2ZLz14s8KPRmVtfJ0d0M3dPhvBp9dJiGNanVXr/pJAY5OS7emKj9uSciGhdkHWXY9Hovw==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-converter": "^3.3.0", + "@tensorflow/tfjs-core": "^3.3.0" + } + }, + "node_modules/@tensorflow-models/deeplab": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@tensorflow-models/deeplab/-/deeplab-0.2.2.tgz", + "integrity": "sha512-tUB16D0fMbVuGR3JF7CXFseMxwnhQ+dtmAmZezT98ask7lVetxhJ5bcsKk/KGAQHcnSgJxZ2S4Cyt2xsrfE9NQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-converter": "^3.0.0", + "@tensorflow/tfjs-core": "^3.0.0" + } + }, + "node_modules/@tensorflow/tfjs": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-3.21.0.tgz", + "integrity": "sha512-khcARd3/872llL/oF4ouR40qlT71mylU66PGT8kHP/GJ5YKj44sv8lDRjU7lOVlJK7jsJFWEsNVHI3eMc/GWNQ==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "3.21.0", + "@tensorflow/tfjs-backend-webgl": "3.21.0", + "@tensorflow/tfjs-converter": "3.21.0", + "@tensorflow/tfjs-core": "3.21.0", + "@tensorflow/tfjs-data": "3.21.0", + "@tensorflow/tfjs-layers": "3.21.0", + "argparse": "^1.0.10", + "chalk": "^4.1.0", + "core-js": "3", + "regenerator-runtime": "^0.13.5", + "yargs": "^16.0.3" + }, + "bin": { + "tfjs-custom-module": "dist/tools/custom_module/cli.js" + } + }, + "node_modules/@tensorflow/tfjs-backend-cpu": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-3.21.0.tgz", + "integrity": "sha512-88S21UAdzyK0CsLUrH17GPTD+26E85OP9CqmLZslaWjWUmBkeTQ5Zqyp6iK+gELnLxPx6q7JsNEeFuPv4254lQ==", + "license": "Apache-2.0", + "dependencies": { + "@types/seedrandom": "^2.4.28", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "3.21.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-webgl": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-3.21.0.tgz", + "integrity": "sha512-N4zitIAT9IX8B8oe489qM3f3VcESxGZIZvHmVP8varOQakTvTX859aaPo1s8hK1qCy4BjSGbweooZe4U8D4kTQ==", + "license": "Apache-2.0", + "dependencies": { + "@tensorflow/tfjs-backend-cpu": "3.21.0", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "@types/webgl-ext": "0.0.30", + "@types/webgl2": "0.0.6", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "3.21.0" + } + }, + "node_modules/@tensorflow/tfjs-backend-webgl/node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs-converter": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-3.21.0.tgz", + "integrity": "sha512-12Y4zVDq3yW+wSjSDpSv4HnpL2sDZrNiGSg8XNiDE4HQBdjdA+a+Q3sZF/8NV9y2yoBhL5L7V4mMLDdbZBd9/Q==", + "license": "Apache-2.0", + "peerDependencies": { + "@tensorflow/tfjs-core": "3.21.0" + } + }, + "node_modules/@tensorflow/tfjs-core": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-3.21.0.tgz", + "integrity": "sha512-YSfsswOqWfd+M4bXIhT3hwtAb+IV8+ODwIxwdFR/7jTAPZP1wMVnSlpKnXHAN64HFOiP+Tm3HmKusEZ0+09A0w==", + "license": "Apache-2.0", + "dependencies": { + "@types/long": "^4.0.1", + "@types/offscreencanvas": "~2019.3.0", + "@types/seedrandom": "^2.4.28", + "@types/webgl-ext": "0.0.30", + "@webgpu/types": "0.1.16", + "long": "4.0.0", + "node-fetch": "~2.6.1", + "seedrandom": "^3.0.5" + }, + "engines": { + "yarn": ">= 1.3.2" + } + }, + "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": { + "version": "2019.3.0", + "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz", + "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q==", + "license": "MIT" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/@webgpu/types": { + "version": "0.1.16", + "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.16.tgz", + "integrity": "sha512-9E61voMP4+Rze02jlTXud++Htpjyyk8vw5Hyw9FGRrmhHQg2GqbuOfwf5Klrb8vTxc2XWI3EfO7RUHMpxTj26A==", + "license": "BSD-3-Clause" + }, + "node_modules/@tensorflow/tfjs-core/node_modules/long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/@tensorflow/tfjs-data": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-3.21.0.tgz", + "integrity": "sha512-eFLfw2wIcFNxnP2Iv/SnVlihehzKMumk1b5Prcx1ixk/SbkCo5u0Lt7OVOWaEOKVqvB2sT+dJcTjAh6lrCC/QA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node-fetch": "^2.1.2", + "node-fetch": "~2.6.1", + "string_decoder": "^1.3.0" + }, + "peerDependencies": { + "@tensorflow/tfjs-core": "3.21.0", + "seedrandom": "^3.0.5" + } + }, + "node_modules/@tensorflow/tfjs-layers": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-3.21.0.tgz", + "integrity": "sha512-CMVXsraakXgnXEnqD9QbtResA7nvV7Jz20pGmjFIodcQkClgmFFhdCG5N+zlVRHEz7VKG2OyfhltZ0dBq/OAhA==", + "license": "Apache-2.0 AND MIT", + "peerDependencies": { + "@tensorflow/tfjs-core": "3.21.0" + } + }, + "node_modules/@tensorflow/tfjs/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", "license": "MIT" @@ -2588,6 +2754,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "license": "MIT" + }, "node_modules/@types/markdown-it": { "version": "14.1.2", "dev": true, @@ -2609,6 +2781,16 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", "license": "MIT" @@ -2659,6 +2841,12 @@ "@types/react": "*" } }, + "node_modules/@types/seedrandom": { + "version": "2.4.34", + "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz", + "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==", + "license": "MIT" + }, "node_modules/@types/stats.js": { "version": "0.17.4", "license": "MIT" @@ -2684,6 +2872,18 @@ "version": "10.0.0", "license": "MIT" }, + "node_modules/@types/webgl-ext": { + "version": "0.0.30", + "resolved": "https://registry.npmjs.org/@types/webgl-ext/-/webgl-ext-0.0.30.tgz", + "integrity": "sha512-LKVgNmBxN0BbljJrVUwkxwRYqzsAEPcZOe6S2T6ZaBDIrFp0qu4FNlpc5sM1tGbXUYFgdVQIoeLk1Y1UoblyEg==", + "license": "MIT" + }, + "node_modules/@types/webgl2": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/webgl2/-/webgl2-0.0.6.tgz", + "integrity": "sha512-50GQhDVTq/herLMiqSQkdtRu+d5q/cWHn4VvKJtrj4DJAjo1MNkWYa2MA41BaBO1q1HgsUjuQvEOk0QHvlnAaQ==", + "license": "MIT" + }, "node_modules/@types/webxr": { "version": "0.5.24", "license": "MIT" @@ -3494,9 +3694,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ansi-styles": { "version": "4.3.0", - "dev": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3660,7 +3868,6 @@ }, "node_modules/chalk": { "version": "4.1.2", - "dev": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", @@ -3715,6 +3922,17 @@ "date-fns": ">=2.0.0" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, "node_modules/clsx": { "version": "2.1.1", "license": "MIT", @@ -3724,7 +3942,6 @@ }, "node_modules/color-convert": { "version": "2.0.1", - "dev": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -3735,7 +3952,6 @@ }, "node_modules/color-name": { "version": "1.1.4", - "dev": true, "license": "MIT" }, "node_modules/combined-stream": { @@ -3767,6 +3983,17 @@ "version": "1.9.0", "license": "MIT" }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cosmiconfig": { "version": "7.1.0", "license": "MIT", @@ -4235,6 +4462,12 @@ "node": ">= 0.4" } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, "node_modules/entities": { "version": "4.5.0", "dev": true, @@ -4330,6 +4563,15 @@ "@esbuild/win32-x64": "0.25.12" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "license": "MIT", @@ -4539,6 +4781,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, "node_modules/get-intrinsic": { "version": "1.3.0", "license": "MIT", @@ -4619,7 +4870,6 @@ }, "node_modules/has-flag": { "version": "4.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -4815,6 +5065,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-hexadecimal": { "version": "1.0.4", "license": "MIT", @@ -5127,6 +5386,26 @@ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/object-assign": { "version": "4.1.1", "license": "MIT", @@ -5716,6 +5995,21 @@ "node": ">=6" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "license": "MIT", @@ -5811,6 +6105,26 @@ "version": "1.3.3", "license": "BSD-3-Clause" }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/safer-buffer": { "version": "2.1.2", "license": "MIT" @@ -5822,6 +6136,12 @@ "loose-envify": "^1.1.0" } }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, "node_modules/semver": { "version": "7.7.3", "dev": true, @@ -5873,6 +6193,12 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stats-gl": { "version": "2.4.2", "license": "MIT", @@ -5893,6 +6219,41 @@ "version": "0.17.0", "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "dev": true, @@ -5910,7 +6271,6 @@ }, "node_modules/supports-color": { "version": "7.2.0", - "dev": true, "license": "MIT", "dependencies": { "has-flag": "^4.0.0" @@ -6007,6 +6367,12 @@ "node": ">=14.14" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, "node_modules/troika-three-text": { "version": "0.52.4", "license": "MIT", @@ -6237,6 +6603,22 @@ "version": "1.1.1", "license": "MIT" }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "license": "ISC", @@ -6258,6 +6640,23 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "dev": true, @@ -6275,6 +6674,15 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaml": { "version": "2.8.2", "license": "ISC", @@ -6288,6 +6696,33 @@ "url": "https://github.com/sponsors/eemeli" } }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/zod": { "version": "3.25.76", "dev": true, diff --git a/frontend/package.json b/frontend/package.json index e222b62..fba67f8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,9 @@ "@tanstack/react-query": "^5.84.1", "@tanstack/react-query-devtools": "^5.84.1", "@tanstack/react-router": "1.19.1", + "@tensorflow-models/coco-ssd": "^2.2.2", + "@tensorflow-models/deeplab": "^0.2.2", + "@tensorflow/tfjs": "^3.21.0", "@types/leaflet": "^1.9.20", "@types/react-grid-layout": "^1.3.5", "@types/uuid": "^10.0.0", diff --git a/frontend/src/components/Common/SidebarItems.tsx b/frontend/src/components/Common/SidebarItems.tsx index 1bbff20..ea43239 100644 --- a/frontend/src/components/Common/SidebarItems.tsx +++ b/frontend/src/components/Common/SidebarItems.tsx @@ -5,7 +5,7 @@ import { Link as RouterLink } from "@tanstack/react-router"; import { useState } from "react"; import { FaBars, FaRobot } from "react-icons/fa"; import { FaLink } from "react-icons/fa"; -import { FiMap, FiSettings, FiUsers } from "react-icons/fi"; +import { FiCode, FiMap, FiSettings, FiUsers } from "react-icons/fi"; import useAuth from "@/hooks/useAuth.ts"; @@ -13,6 +13,7 @@ const items = [ { icon: FaRobot, title: "Robots", path: "/robots" }, { icon: FiMap, title: "Occupancy Maps", path: "/occupancy-maps" }, { icon: FaLink, title: "Dashboard", path: "/dashboard" }, + { icon: FiCode, title: "Widget Test", path: "/widget-customize" }, { icon: FiSettings, title: "User Settings", path: "/settings" }, ]; diff --git a/frontend/src/components/Dashboard/DashboardGrid.tsx b/frontend/src/components/Dashboard/DashboardGrid.tsx index 47c5176..b93dc53 100644 --- a/frontend/src/components/Dashboard/DashboardGrid.tsx +++ b/frontend/src/components/Dashboard/DashboardGrid.tsx @@ -16,7 +16,7 @@ import { type Layout, Responsive, WidthProvider } from "react-grid-layout"; import { getTabConfigApi, updateTabConfigApi } from "@/client/service/dashboard.api.ts"; import RobotConnectionPanel from "@/components/Dashboard/RobotConnectionPanel.tsx"; -import { WidgetFactory } from "@/components/Dashboard/WidgetFactory.tsx"; +import { WidgetFactory } from "@/components/Dashboard/Widgets/WidgetFactory.tsx"; import useAuth from "@/hooks/useAuth.ts"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; @@ -24,6 +24,7 @@ import { useMosaicWebRTCConnection } from "@/hooks/useMosaicWebRTCConnection.ts" import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; import { RobotConnector, type TabConfig, type WidgetConfig } from "@/mosaic"; import { DASHBOARD_STORAGE_KEYS } from "@/utils"; +import { getWidgetDescriptor } from "@/widgets/_utils/widgetRegistry.ts"; const ResponsiveGridLayout = WidthProvider(Responsive); @@ -83,29 +84,6 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) { const editableWidgetsRef = useRef([]); const isLayoutDirtyRef = useRef(false); - const { data: tabConfig, isPending: isConfigLoading } = useQuery({ - queryKey: ["parsedDashboardTabConfig", tabId], - queryFn: async () => { - const tabConfigDto = await getTabConfigApi(tabId); - const widgets = JSON.parse(tabConfigDto.widgets).widgets as WidgetConfig[]; - return { - id: tabConfigDto.id, - name: tabConfigDto.name, - widgets: widgets.map((widget) => { - return { - id: widget.id, - type: widget.type, - position: widget.position, - connectors: widget.connectors.map((connector) => { - return new RobotConnector(connector.robotId, connector.connectorId); - }), - }; - }), - } as TabConfig; - }, - enabled: !!user, - }); - const saveLayoutMutation = useMutation({ mutationFn: async ({ targetTabId, @@ -142,6 +120,58 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) { }, }); + const onUpdateWidgetParams = (widgetId: string, params?: any) => { + if (!tabConfig) { + return; + } + + const newWidgets = editableWidgetsRef.current.map((widget) => { + if (widget.id !== widgetId) { + return widget; + } + + return { + ...widget, + params: params, + }; + }); + + saveLayoutMutation.mutate({ + targetTabId: tabConfig.id, + widgets: newWidgets, + }); + }; + + const { data: tabConfig, isPending: isConfigLoading } = useQuery({ + queryKey: ["parsedDashboardTabConfig", tabId], + queryFn: async () => { + const tabConfigDto = await getTabConfigApi(tabId); + const widgets = JSON.parse(tabConfigDto.widgets).widgets as WidgetConfig[]; + return { + id: tabConfigDto.id, + name: tabConfigDto.name, + widgets: widgets.map((widget) => { + return { + id: widget.id, + type: widget.type, + position: widget.position, + connectors: widget.connectors.map((connector) => { + return new RobotConnector(connector.robotId, connector.connectorId); + }), + params: { + ...getWidgetDescriptor(widget.type)?.getDefaultParams(), + ...widget.params, + }, + onUpdateWidgetParams: (params?: any) => { + onUpdateWidgetParams(widget.id, params); + }, + }; + }), + } as TabConfig; + }, + enabled: !!user, + }); + useEffect(() => { if (!tabConfig) { setEditableWidgets([]); @@ -343,22 +373,21 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) { onLayoutChange={handleLayoutChange} onDragStop={(layout) => handleLayoutCommit(layout)} onResizeStop={(layout) => handleLayoutCommit(layout)} - isDraggable={true} - isResizable={true} - margin={[16, 16]} + isDraggable + isResizable draggableHandle=".draggable-header" > {editableWidgets.map((widgetConfig) => ( diff --git a/frontend/src/components/Dashboard/WidgetFactory.tsx b/frontend/src/components/Dashboard/WidgetFactory.tsx deleted file mode 100644 index c824e83..0000000 --- a/frontend/src/components/Dashboard/WidgetFactory.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { type ComponentType, type LazyExoticComponent, Suspense, lazy } from "react"; - -import type { WidgetConfig } from "@/mosaic"; - -type WidgetComponent = ComponentType<{ widgetConfig: WidgetConfig }>; - -const modules = import.meta.glob("../../widgets/*/*.tsx"); -const registry = new Map>(); -const LazyNotFoundWidget = lazy( - () => import("../../widgets/NotFoundWidget/NotFoundWidget.tsx"), -) as LazyExoticComponent; - -function getLazyWidget(type: string) { - if (registry.has(type)) return registry.get(type)!; - - const entry = Object.entries(modules).find(([path]) => path.includes(`/${type}/${type}.tsx`)); - if (!entry) return null; - - const component = lazy(entry[1] as () => Promise<{ default: WidgetComponent }>); - registry.set(type, component); - return component; -} - -export interface WidgetFactoryProps { - widgetConfig: WidgetConfig; -} - -export function WidgetFactory({ widgetConfig }: WidgetFactoryProps) { - const LazyComponent = getLazyWidget(widgetConfig.type) ?? LazyNotFoundWidget; - - return ( - Loading...}> - - - ); -} diff --git a/frontend/src/components/Dashboard/WidgetFooter.tsx b/frontend/src/components/Dashboard/WidgetFooter.tsx deleted file mode 100644 index 40d396f..0000000 --- a/frontend/src/components/Dashboard/WidgetFooter.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { ReactNode } from "react"; - -import { HStack, Text, VStack } from "@chakra-ui/react"; - -interface WidgetFooterProps { - footerInfo?: Array<{ - label: string; - value: string | ReactNode; - }>; - footerMessage?: string; -} - -export function WidgetFooter({ footerInfo, footerMessage }: WidgetFooterProps) { - return ( - <> - {/* Footer Info */} - {footerInfo && footerInfo.length > 0 && ( - - {footerInfo.map((info, index) => ( - - - {info.label} - - {typeof info.value === "string" ? ( - - {info.value} - - ) : ( - info.value - )} - - ))} - - )} - {footerMessage && ( - - {footerMessage} - - )} - - ); -} diff --git a/frontend/src/components/Dashboard/WidgetFrame.tsx b/frontend/src/components/Dashboard/WidgetFrame.tsx deleted file mode 100644 index 47d918a..0000000 --- a/frontend/src/components/Dashboard/WidgetFrame.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { ReactNode } from "react"; - -import { VStack } from "@chakra-ui/react"; - -import type { WidgetConfig } from "@/mosaic"; - -import { WidgetBody } from "./WidgetBody"; -import { WidgetFooter } from "./WidgetFooter"; -import { WidgetHeader } from "./WidgetHeader"; - -export interface WidgetFrameProps { - widgetConfig: WidgetConfig; - children?: ReactNode; - useBody?: boolean; - showRobotInfo?: boolean; - footerInfo?: Array<{ - label: string; - value: string | ReactNode; - }>; - footerMessage?: string; -} - -export function WidgetFrame({ - widgetConfig, - children, - useBody = true, - showRobotInfo = true, - footerInfo = [], - footerMessage, -}: WidgetFrameProps) { - return ( - - - {useBody ? {children} : children} - - - ); -} diff --git a/frontend/src/components/Dashboard/WidgetHeader.tsx b/frontend/src/components/Dashboard/WidgetHeader.tsx deleted file mode 100644 index e3b627d..0000000 --- a/frontend/src/components/Dashboard/WidgetHeader.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Flex, HStack, Text } from "@chakra-ui/react"; - -import type { WidgetConfig } from "@/mosaic"; - -import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; - -export interface WidgetHeaderProps { - widgetConfig: WidgetConfig; - showRobotInfo?: boolean; -} - -export function WidgetHeader({ - widgetConfig: { type, connectors }, - showRobotInfo = true, -}: WidgetHeaderProps) { - const { robotInfos } = useRobotInfo(); - - return ( - - - - - {type} - - {showRobotInfo && ( - - Robot:{" "} - {connectors - .map((c) => { - const robotInfo = robotInfos.find((robotInfos) => robotInfos.id === c.robotId); - if (robotInfo) return robotInfo.name; - return c.robotId; - }) - .join(", ")} - - )} - - - - {/*{onRemove && (*/} - {/* {*/} - {/* e.stopPropagation()*/} - {/* onRemove()*/} - {/* }}*/} - {/* >*/} - {/* Remove*/} - {/* */} - {/*)}*/} - {/*{onSettingClick && (*/} - {/* {*/} - {/* e.stopPropagation()*/} - {/* onSettingClick()*/} - {/* }}*/} - {/* >*/} - {/* */} - {/* */} - {/*)}*/} - - ); -} diff --git a/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx b/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx new file mode 100644 index 0000000..cce7c0e --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx @@ -0,0 +1,46 @@ +import { Box, Code, Flex, HStack, Icon, Text, VStack } from "@chakra-ui/react"; +import { LuTriangleAlert } from "react-icons/lu"; + +export interface ErrorWidgetProps { + error: string; +} + +export function ErrorWidget({ error }: ErrorWidgetProps) { + return ( + + + + + + + + + + Widget Error + + + This widget could not be loaded due to an error. + + + + + + Error + + + {error} + + + + + + ); +} diff --git a/frontend/src/components/Dashboard/Widgets/MosaicWidgetContext.tsx b/frontend/src/components/Dashboard/Widgets/MosaicWidgetContext.tsx new file mode 100644 index 0000000..4e775bf --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/MosaicWidgetContext.tsx @@ -0,0 +1,15 @@ +import { createContext, useContext } from "react"; + +import type { WidgetConfig } from "@/mosaic"; + +const MosaicWidgetContext = createContext(null); + +export function useMosaicWidget(): WidgetConfig { + const ctx = useContext(MosaicWidgetContext); + if (!ctx) { + throw new Error("useMosaicWidget must be used within MosaicWidget.Root"); + } + return ctx; +} + +export { MosaicWidgetContext }; diff --git a/frontend/src/components/Dashboard/WidgetBody.tsx b/frontend/src/components/Dashboard/Widgets/WidgetBody.tsx similarity index 84% rename from frontend/src/components/Dashboard/WidgetBody.tsx rename to frontend/src/components/Dashboard/Widgets/WidgetBody.tsx index 88deb56..a959e48 100644 --- a/frontend/src/components/Dashboard/WidgetBody.tsx +++ b/frontend/src/components/Dashboard/Widgets/WidgetBody.tsx @@ -9,12 +9,11 @@ export interface WidgetBodyProps { export function WidgetBody({ children }: WidgetBodyProps) { return ( isValidElement(child) && child.type === WidgetHeader, + ); + const foundBody = Children.toArray(children).find( + (child) => isValidElement(child) && child.type === WidgetBody, + ); + + return ( + + + {foundHeader || } + {error ? ( + + + + ) : ( + foundBody || ( + + + + ) + )} + + + ); +} + +export { useMosaicWidget } from "@/components/Dashboard/Widgets/MosaicWidgetContext.tsx"; + +export const MosaicWidget = { + Root: WidgetRoot, + Header: WidgetHeader, + SettingDialog: WidgetSettingDialog, + Body: WidgetBody, +}; diff --git a/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts new file mode 100644 index 0000000..7d8cbfa --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts @@ -0,0 +1,41 @@ +import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts"; + +export abstract class WidgetDescriptor> { + public abstract getName(): string; + + public getRequiredStoreType(): StoreType | null { + return null; + } + + public getMinStoreNumber(): number { + return 1; + } + + public getMaxStoreNumber(): number { + return 1; + } + + public getMinRobotConnectorNumber(): number { + return 1; + } + + public getMaxRobotConnectorNumber(): number { + return 1; + } + + public supportCustomParams(): boolean { + return false; + } + + public getDefaultParams(): T { + return {} as T; + } + + public validateParams(_params: T): string | null { + return null; + } + + public getDefaultInjectData(): string { + return ""; + } +} diff --git a/frontend/src/components/Dashboard/Widgets/WidgetFactory.tsx b/frontend/src/components/Dashboard/Widgets/WidgetFactory.tsx new file mode 100644 index 0000000..f6a5061 --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetFactory.tsx @@ -0,0 +1,51 @@ +import { type ComponentType, type LazyExoticComponent, Suspense, lazy } from "react"; + +import type { WidgetConfig } from "@/mosaic"; + +import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; +import { getWidgetDescriptor, getWidgetModuleLoader } from "@/widgets/_utils/widgetRegistry.ts"; + +type WidgetComponent = ComponentType<{ widgetConfig: WidgetConfig }>; + +const componentCache = new Map>(); + +const LazyNotFoundWidget = lazy( + () => import("../../../widgets/NotFoundWidget/NotFoundWidget.tsx"), +) as LazyExoticComponent; + +function getLazyWidget(type: string) { + const descriptor = getWidgetDescriptor(type); + if (!descriptor) return null; + + if (componentCache.has(type)) return componentCache.get(type)!; + + const name = descriptor.getName(); + const moduleLoader = getWidgetModuleLoader(name); + if (!moduleLoader) return null; + + const component = lazy(moduleLoader as () => Promise<{ default: WidgetComponent }>); + componentCache.set(type, component); + return component; +} + +export interface WidgetFactoryProps { + widgetConfig: WidgetConfig; +} + +export function WidgetFactory({ widgetConfig }: WidgetFactoryProps) { + const descriptor = getWidgetDescriptor(widgetConfig.type); + if (descriptor) { + const validationError = descriptor.validateParams(widgetConfig.params ?? {}); + if (validationError) { + return ; + } + } + + const LazyComponent = getLazyWidget(widgetConfig.type) ?? LazyNotFoundWidget; + + return ( + Loading...}> + + + ); +} diff --git a/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx b/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx new file mode 100644 index 0000000..de6384d --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx @@ -0,0 +1,73 @@ +import type { ReactNode } from "react"; + +import { Box, Flex, HStack, Icon, Text, VStack } from "@chakra-ui/react"; +import { FiInfo } from "react-icons/fi"; + +import { Tooltip } from "@/components/ui/tooltip.tsx"; +import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; + +import { useMosaicWidget } from "./MosaicWidgetContext.tsx"; + +export interface AdditionalInfo { + label: string; + value: string; +} + +export interface WidgetHeaderProps { + children?: ReactNode; + additionalInfo?: AdditionalInfo[]; +} + +export function WidgetHeader({ children, additionalInfo }: WidgetHeaderProps) { + const { type, connectors } = useMosaicWidget(); + const { robotInfos } = useRobotInfo(); + + const tooltipContent = ( + + + Connected Robots + + {connectors.map((c, i) => { + const robotInfo = robotInfos.find((r) => r.id === c.robotId); + return ( + + {robotInfo?.name ?? c.robotId} + + {c.connectorId} + + + ); + })} + {additionalInfo && additionalInfo.length > 0 && ( + + + {additionalInfo.map((info, i) => ( + + {info.label} + {info.value} + + ))} + + + )} + + ); + + return ( + + + + {type} + + + + + + {children && ( + e.stopPropagation()}> + {children} + + )} + + ); +} diff --git a/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx b/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx new file mode 100644 index 0000000..20ea4c4 --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx @@ -0,0 +1,97 @@ +import { Button, DialogActionTrigger, DialogTitle } from "@chakra-ui/react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ReactNode, useRef, useState } from "react"; +import { FieldValues, type SubmitHandler, UseFormReturn } from "react-hook-form"; +import { IoSettings } from "react-icons/io5"; + +import type { ApiError } from "@/client"; + +import { + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTrigger, +} from "@/components/ui/dialog.tsx"; +import useCustomToast from "@/hooks/useCustomToast.ts"; +import { handleError } from "@/utils"; + +import { useMosaicWidget } from "./MosaicWidgetContext.tsx"; + +export interface WidgetSettingDialogProps { + children?: ReactNode; + useFormReturn: UseFormReturn; +} + +export function WidgetSettingDialog({ + children, + useFormReturn: { + handleSubmit, + reset, + formState: { isValid, isSubmitting }, + }, +}: WidgetSettingDialogProps) { + const widgetConfig = useMosaicWidget(); + const { showSuccessToast } = useCustomToast(); + const [isOpen, setIsOpen] = useState(false); + const dialogContentRef = useRef(null); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (data: T) => { + widgetConfig.onUpdateWidgetParams(data); + }, + onSuccess: (_, variables) => { + showSuccessToast("Settings saved successfully."); + reset(variables); + setIsOpen(false); + }, + onError: (err: ApiError) => { + handleError(err); + }, + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: ["robots"] }); + }, + }); + + const onSubmit: SubmitHandler = (data: T) => { + mutation.mutate(data); + }; + + return ( + setIsOpen(open)} + > + + + + +
+ + Change Setting + + {children} + + + + + + + +
+ +
+
+ ); +} diff --git a/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx b/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx index 17cc8ca..b6bd8d1 100644 --- a/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx +++ b/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx @@ -41,7 +41,7 @@ export function OccupancyMapsTable() { const setPage = (page: number) => navigate({ - search: (prev: { [key: string]: string }) => ({ ...prev, page }), + search: (prev) => ({ ...prev, page }), }); const occupancyMaps = data?.data.slice(0, PER_PAGE) ?? []; diff --git a/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx b/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx new file mode 100644 index 0000000..09aed2f --- /dev/null +++ b/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx @@ -0,0 +1,173 @@ +import { Badge, Box, Button, Code, HStack, Input, Text, Textarea, VStack } from "@chakra-ui/react"; +import { useState } from "react"; + +interface ReceivableDataPanelProps { + onInject: (rawData: string) => void; + defaultData: string; +} + +function tryFormatJson(value: string): string { + try { + return JSON.stringify(JSON.parse(value), null, 2); + } catch { + return value; + } +} + +function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps) { + const [input, setInput] = useState(() => tryFormatJson(defaultData)); + + return ( + + + Inject raw data (simulates data arriving from the robot) + +