From 3006f621207357ebfa0abcfb4e6fe6dda25ade6a Mon Sep 17 00:00:00 2001 From: Amal Nanavati <amaln@uw.edu> Date: Mon, 15 Jan 2024 19:39:45 -0800 Subject: [PATCH] Allow users to customize the distance to mouth during bite transfer (#113) * [WIP] Implemented getting/setting parameter TODO: - Implement "Try It" along with error handling - Test it * Separated DetectingFace into two components for easier reuse * Generalized RobotMotion to be called within Settings * Remove dependency on generic props * Finished implementing MVP bite transfer customizatipn * Clip allowable distances to mouth * Finalized it in sim * Removed combo MoveFromMouthTo..., made 'Done' restore pre-Settings robot arm configuration * Small fixes from sim testing * Formatting --- feedingwebapp/package-lock.json | 1874 ++++++++++++++++- feedingwebapp/package.json | 1 + feedingwebapp/src/Pages/Constants.js | 20 +- feedingwebapp/src/Pages/Footer/Footer.jsx | 47 +- feedingwebapp/src/Pages/GlobalState.jsx | 108 +- feedingwebapp/src/Pages/Header/Header.jsx | 21 +- feedingwebapp/src/Pages/Home/Home.jsx | 88 +- .../src/Pages/Home/MealStates/BiteDone.jsx | 176 +- .../Home/MealStates/CircleProgressBar.jsx | 2 +- .../Pages/Home/MealStates/DetectingFace.jsx | 134 +- .../MealStates/DetectingFaceSubcomponent.jsx | 152 ++ .../src/Pages/Home/MealStates/RobotMotion.jsx | 85 +- feedingwebapp/src/Pages/Home/VideoFeed.jsx | 7 +- .../src/Pages/Settings/BiteTransfer.jsx | 510 +++++ feedingwebapp/src/Pages/Settings/Main.jsx | 112 + feedingwebapp/src/Pages/Settings/Settings.jsx | 88 +- feedingwebapp/src/ros/ros_helpers.js | 32 + feedingwebapp/src/webrtc/webrtc_helpers.js | 4 +- 18 files changed, 2975 insertions(+), 486 deletions(-) create mode 100644 feedingwebapp/src/Pages/Home/MealStates/DetectingFaceSubcomponent.jsx create mode 100644 feedingwebapp/src/Pages/Settings/BiteTransfer.jsx create mode 100644 feedingwebapp/src/Pages/Settings/Main.jsx diff --git a/feedingwebapp/package-lock.json b/feedingwebapp/package-lock.json index d4485108..a44f5970 100644 --- a/feedingwebapp/package-lock.json +++ b/feedingwebapp/package-lock.json @@ -8,6 +8,7 @@ "name": "feedingwebapp", "version": "0.1.0", "dependencies": { + "@fluentui/react-components": "^9.44.1", "@mapbox/node-pre-gyp": "^1.0.11", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", @@ -2219,74 +2220,1817 @@ "postcss-selector-parser": "^6.0.10" } }, + "node_modules/@emotion/hash": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" + }, "node_modules/@emotion/is-prop-valid": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", "dependencies": { - "@emotion/memoize": "^0.8.1" + "@emotion/memoize": "^0.8.1" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "node_modules/@emotion/stylis": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", + "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", + "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", + "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.5.2", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.40.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", + "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.5.3.tgz", + "integrity": "sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==", + "dependencies": { + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/devtools": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@floating-ui/devtools/-/devtools-0.0.4.tgz", + "integrity": "sha512-lSlvB45PgGS+YmGtOPseYBLwa+u+wPsZ+g/bT5kgzK2zLAKR5m9L5hS3b/OFZhrFWwehRf73RRSZ/WXg4r5WMw==", + "peerDependencies": { + "@floating-ui/dom": ">=1.5.4" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.5.4.tgz", + "integrity": "sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==", + "dependencies": { + "@floating-ui/core": "^1.5.3", + "@floating-ui/utils": "^0.2.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.1.tgz", + "integrity": "sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==" + }, + "node_modules/@fluentui/keyboard-keys": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@fluentui/keyboard-keys/-/keyboard-keys-9.0.7.tgz", + "integrity": "sha512-vaQ+lOveQTdoXJYqDQXWb30udSfTVcIuKk1rV0X0eGAgcHeSDeP1HxMy+OgHOQZH3OiBH4ZYeWxb+tmfiDiygQ==", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/keyboard-keys/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/priority-overflow": { + "version": "9.1.11", + "resolved": "https://registry.npmjs.org/@fluentui/priority-overflow/-/priority-overflow-9.1.11.tgz", + "integrity": "sha512-sdrpavvKX2kepQ1d6IaI3ObLq5SAQBPRHPGx2+wiMWL7cEx9vGGM0fmeicl3soqqmM5uwCmWnZk9QZv9XOY98w==", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/priority-overflow/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-accordion": { + "version": "9.3.34", + "resolved": "https://registry.npmjs.org/@fluentui/react-accordion/-/react-accordion-9.3.34.tgz", + "integrity": "sha512-kg6qUmDKFi2hY/HAcRIPpeXafOySHJcU5TguOoX1NCDfd0k9hj6facCD+b3uPt6HXBEALETAg0udcu+qNuSyPQ==", + "dependencies": { + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-accordion/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-alert": { + "version": "9.0.0-beta.99", + "resolved": "https://registry.npmjs.org/@fluentui/react-alert/-/react-alert-9.0.0-beta.99.tgz", + "integrity": "sha512-2uWDRjAypS3yU6tmr57dqn8YdSAtR+BHE9n+kCsIprO52tBoFTrf27HZAITzLqCNSDf5X43K3k66ijJ7VZPFcw==", + "dependencies": { + "@fluentui/react-avatar": "^9.6.4", + "@fluentui/react-button": "^9.3.61", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-alert/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-aria": { + "version": "9.6.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-aria/-/react-aria-9.6.2.tgz", + "integrity": "sha512-DMM4l5fMfg7yltqM33TFlEJlua7eAqIdLnKtnQ4szezbG2QZOy+a+qmJOGNcScteuO1/kRfYC+WOXhxtnMeA7g==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-utilities": "^9.15.6", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-aria/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-avatar": { + "version": "9.6.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-avatar/-/react-avatar-9.6.4.tgz", + "integrity": "sha512-ziQ2KshcoV5rccmB1X2Hl0IYWJEor0OEcX2e4ki5BgCEI2JbYtd/rKQvw60kYyddqU3Vb7u9fThtxBu3o806eA==", + "dependencies": { + "@fluentui/react-badge": "^9.2.19", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-popover": "^9.8.28", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-tooltip": "^9.4.6", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-avatar/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-badge": { + "version": "9.2.19", + "resolved": "https://registry.npmjs.org/@fluentui/react-badge/-/react-badge-9.2.19.tgz", + "integrity": "sha512-6HjUNeNV1KkSTuft+8YtPd5tfbJKJA5g0HO/8+M7h803cr0zhyyokPOZuKM/tH5r9VCWa+gPoyaHRZUiWyHqIA==", + "dependencies": { + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-badge/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-breadcrumb": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-breadcrumb/-/react-breadcrumb-9.0.7.tgz", + "integrity": "sha512-/Kmp8CJxpW7ymTHAhSi9e09btPRhZG//oaSP6AGtTR5/0A8N1Kd+rDfmlvTleRWDn8+MMth+azt+amRaenNouQ==", + "dependencies": { + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-button": "^9.3.61", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-link": "^9.2.4", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-breadcrumb/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-button": { + "version": "9.3.61", + "resolved": "https://registry.npmjs.org/@fluentui/react-button/-/react-button-9.3.61.tgz", + "integrity": "sha512-QYNZj6CxCbOJ0wKwhDx2QaAW4udS+vw4hlLcUUHZGnksYXrJt9vhKcflokShLpOKQikD9cPn+T8b5QzFGy2Skw==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-button/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-card": { + "version": "9.0.60", + "resolved": "https://registry.npmjs.org/@fluentui/react-card/-/react-card-9.0.60.tgz", + "integrity": "sha512-ykohMAPtGor2RXZJeQFjwitVhhu03cq6I79WTgHnQIQvwyZ9RiHHYJ8QKsQzDpdyleSqOcUA6O6yk3oiVKVjNw==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-card/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-checkbox": { + "version": "9.2.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-checkbox/-/react-checkbox-9.2.5.tgz", + "integrity": "sha512-lfodSwu83BeDPaZRHNftqWmIrv8m9SeHdOODuAp1VPDWUCNtZkajZZaHXv77RQRKsF/O4Q60bOy0QtTsRkTqzg==", + "dependencies": { + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-checkbox/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-combobox": { + "version": "9.5.38", + "resolved": "https://registry.npmjs.org/@fluentui/react-combobox/-/react-combobox-9.5.38.tgz", + "integrity": "sha512-AtvzKYCkzm0zSWSstx+q42HkejmZNfO3iH++fD3Z94e1jDdUD+KSLBpCTumskTvPLWPE6LlqhyYDKPjBK3bbOg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-portal": "^9.4.7", + "@fluentui/react-positioning": "^9.11.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-combobox/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-components": { + "version": "9.44.1", + "resolved": "https://registry.npmjs.org/@fluentui/react-components/-/react-components-9.44.1.tgz", + "integrity": "sha512-Qc3ItWXXsE4MdfzCGuzbqfA4StD8zijKOdlgPo7KrJdJ6hcXI7sDmkOMmjRJmcrDMyKPvoIPENr8emGqFbdklA==", + "dependencies": { + "@fluentui/react-accordion": "^9.3.34", + "@fluentui/react-alert": "9.0.0-beta.99", + "@fluentui/react-avatar": "^9.6.4", + "@fluentui/react-badge": "^9.2.19", + "@fluentui/react-breadcrumb": "^9.0.7", + "@fluentui/react-button": "^9.3.61", + "@fluentui/react-card": "^9.0.60", + "@fluentui/react-checkbox": "^9.2.5", + "@fluentui/react-combobox": "^9.5.38", + "@fluentui/react-dialog": "^9.9.3", + "@fluentui/react-divider": "^9.2.55", + "@fluentui/react-drawer": "^9.0.7", + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-image": "^9.1.52", + "@fluentui/react-infobutton": "9.0.0-beta.83", + "@fluentui/react-infolabel": "^9.0.11", + "@fluentui/react-input": "^9.4.57", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-link": "^9.2.4", + "@fluentui/react-menu": "^9.12.40", + "@fluentui/react-message-bar": "^9.0.12", + "@fluentui/react-overflow": "^9.1.5", + "@fluentui/react-persona": "^9.2.63", + "@fluentui/react-popover": "^9.8.28", + "@fluentui/react-portal": "^9.4.7", + "@fluentui/react-positioning": "^9.11.0", + "@fluentui/react-progress": "^9.1.57", + "@fluentui/react-provider": "^9.13.5", + "@fluentui/react-radio": "^9.2.0", + "@fluentui/react-select": "^9.1.57", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-skeleton": "^9.0.45", + "@fluentui/react-slider": "^9.1.62", + "@fluentui/react-spinbutton": "^9.2.57", + "@fluentui/react-spinner": "^9.3.35", + "@fluentui/react-switch": "^9.1.62", + "@fluentui/react-table": "^9.11.0", + "@fluentui/react-tabs": "^9.4.3", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-tags": "^9.0.17", + "@fluentui/react-text": "^9.4.4", + "@fluentui/react-textarea": "^9.3.57", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-toast": "^9.3.23", + "@fluentui/react-toolbar": "^9.1.62", + "@fluentui/react-tooltip": "^9.4.6", + "@fluentui/react-tree": "^9.4.20", + "@fluentui/react-utilities": "^9.15.6", + "@fluentui/react-virtualizer": "9.0.0-alpha.63", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-components/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-context-selector": { + "version": "9.1.46", + "resolved": "https://registry.npmjs.org/@fluentui/react-context-selector/-/react-context-selector-9.1.46.tgz", + "integrity": "sha512-CDoGob3p9ZRL7AwzrxAjTPHJIjGvdMOfa+oIFL7O6EHATvU/7BQUPcCRPvhfZK4N4jD1w7AJgYwPPgkOqYbHpw==", + "dependencies": { + "@fluentui/react-utilities": "^9.15.6", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-context-selector/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-dialog": { + "version": "9.9.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-dialog/-/react-dialog-9.9.3.tgz", + "integrity": "sha512-WAQgrbt/I1X0XcLDnMt+qmAx30E3Ol/auJhACLhilkht/uQ6xoeWOjRGBY2k6yMA32QpLqdGbV4UA4EcLLBuSg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-portal": "^9.4.7", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-dialog/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-divider": { + "version": "9.2.55", + "resolved": "https://registry.npmjs.org/@fluentui/react-divider/-/react-divider-9.2.55.tgz", + "integrity": "sha512-WbV5nDhqe+2JXq4Igv7eZHrAB4v+CbICGhJ+sY86uHd9872hjBr9cHUSpBnY8yeffXc/PSmjF+i1NcmCOyHUyg==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-divider/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-drawer": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-drawer/-/react-drawer-9.0.7.tgz", + "integrity": "sha512-XKG5m+vFQvBl92R9O6LQOUMPSOqzh1KsR3iZQehfhcticEuShi0uIK9ys0/atYJ890ErWjuXPF7cJHvNU5IYXg==", + "dependencies": { + "@fluentui/react-dialog": "^9.9.3", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-motion-preview": "^0.5.7", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-drawer/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-field": { + "version": "9.1.47", + "resolved": "https://registry.npmjs.org/@fluentui/react-field/-/react-field-9.1.47.tgz", + "integrity": "sha512-FXVjdHkhEVr8bRKoKoZRFvrW5ZAU3ZRq4EUxMvZDOxfeVx0cxo8qIG2BOp2xe4GrVXyfVtq0Fguqx58ttlG5sg==", + "dependencies": { + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-field/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-icons": { + "version": "2.0.224", + "resolved": "https://registry.npmjs.org/@fluentui/react-icons/-/react-icons-2.0.224.tgz", + "integrity": "sha512-ld03dlc1pG7xeTQsK1y5in19jkMtWHNaoktFv+e7NV2xmrgV/SgPyyjEXbMrQHb8Naea/XCoCpNHnUNCy68akw==", + "dependencies": { + "@griffel/react": "^1.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-image": { + "version": "9.1.52", + "resolved": "https://registry.npmjs.org/@fluentui/react-image/-/react-image-9.1.52.tgz", + "integrity": "sha512-gKlf1LJXAgvyUrYlskxk58ZHMF05v0tk21G9HMWczEiivThgigsrwIRtGC2jJI66Op8c5VLdZ8/wrNK5s43k2Q==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-image/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-infobutton": { + "version": "9.0.0-beta.83", + "resolved": "https://registry.npmjs.org/@fluentui/react-infobutton/-/react-infobutton-9.0.0-beta.83.tgz", + "integrity": "sha512-qPY9DSBv70jTl5M6YJMEYDyVPsD5D47GsTmKiuaohm9xMjXs3JUpnmve9E0uTCwQmxKRt57qvMfBSD3PCpdO7Q==", + "dependencies": { + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-popover": "^9.8.28", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-infobutton/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-infolabel": { + "version": "9.0.11", + "resolved": "https://registry.npmjs.org/@fluentui/react-infolabel/-/react-infolabel-9.0.11.tgz", + "integrity": "sha512-St74bRh14W+m5ClHIMJTqqut0PCtgo18Of1G9fQBAs7hEQuTMtj8hPWJHJsXAl70CovjaBZg3cADfXGS/WtfYw==", + "dependencies": { + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-popover": "^9.8.28", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-infolabel/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-input": { + "version": "9.4.57", + "resolved": "https://registry.npmjs.org/@fluentui/react-input/-/react-input-9.4.57.tgz", + "integrity": "sha512-zWcUzHQitMR6etKn2b0BvqjSiZxyX3LQ5XEdG2VW2QyXyF/sa+NTEOxiyqSo9Gk4wS3TO0WQb/3cPyMiubSWYQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-input/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-jsx-runtime": { + "version": "9.0.24", + "resolved": "https://registry.npmjs.org/@fluentui/react-jsx-runtime/-/react-jsx-runtime-9.0.24.tgz", + "integrity": "sha512-aWnu04Cfz7RmRYuL/ra2V5vfH0KVjP6dBAy6I+MntxY4m9QwyJ3cS0Z/CzqmexW57w2VjR8+J5uNGATSlwcuuw==", + "dependencies": { + "@fluentui/react-utilities": "^9.15.6", + "@swc/helpers": "^0.5.1", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-jsx-runtime/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-label": { + "version": "9.1.55", + "resolved": "https://registry.npmjs.org/@fluentui/react-label/-/react-label-9.1.55.tgz", + "integrity": "sha512-lgWsw0F0g8qa5lHITvWbJpEinvhr5iMlX8/O73ynn2N4tt1Ltqx9HPld/J5E40s3KUZQCgYgTwCu3bw3/AbT5w==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-label/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-link": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-link/-/react-link-9.2.4.tgz", + "integrity": "sha512-zO/DONs1ay5B2VfYH6pfZVsjSanKmMI1S0HqSk7pdGtkp8XhwRFl+G3Xm+v9J4aKfvakJEMemTCRNcHiuCwwsA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-link/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-menu": { + "version": "9.12.40", + "resolved": "https://registry.npmjs.org/@fluentui/react-menu/-/react-menu-9.12.40.tgz", + "integrity": "sha512-GLjAjTRiO1dByfr5yGPrO7rA3HgwkbVCV4j8t8vh64yi3I0cmnXC7S3Wv2KOD0oIVMDnwEozk3kBvjmy3RfwAw==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-portal": "^9.4.7", + "@fluentui/react-positioning": "^9.11.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-menu/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-message-bar": { + "version": "9.0.12", + "resolved": "https://registry.npmjs.org/@fluentui/react-message-bar/-/react-message-bar-9.0.12.tgz", + "integrity": "sha512-/ysREhZ0CckNukrRr5S6SNdXJBJWxNlJoYuzYLFxiRV9xj05+EXpuoKxsQwaG5LMkl5I+x+tf3WuPBKJs9FcXA==", + "dependencies": { + "@fluentui/react-button": "^9.3.61", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-message-bar/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-motion-preview": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-motion-preview/-/react-motion-preview-0.5.7.tgz", + "integrity": "sha512-RODcicznqfrMzHTwrs62JCOi9S+vS/W1VOJ/KQFS7SLh7DgyZuNRz6mqZwbZMy3xHibli4qITMnLrwOzBg7Lhg==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-motion-preview/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-overflow": { + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-overflow/-/react-overflow-9.1.5.tgz", + "integrity": "sha512-8qdOL8MSnT0UeXRY9gVnzKgMx/LqmStTqGsY+luKDgmXxkzlc2hqx9SLnByry1ljDPVpI1LtOku9U6YZhGeg4g==", + "dependencies": { + "@fluentui/priority-overflow": "^9.1.11", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-overflow/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-persona": { + "version": "9.2.63", + "resolved": "https://registry.npmjs.org/@fluentui/react-persona/-/react-persona-9.2.63.tgz", + "integrity": "sha512-W0eqsXr1M2WhLuxYU1pi/5dsZnM/zF6HPUuU7k/kSZF5Psi+ad9mYSOo6xKVgQ8UCaX5acJpkIYa+ExpgwJP3w==", + "dependencies": { + "@fluentui/react-avatar": "^9.6.4", + "@fluentui/react-badge": "^9.2.19", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-persona/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-popover": { + "version": "9.8.28", + "resolved": "https://registry.npmjs.org/@fluentui/react-popover/-/react-popover-9.8.28.tgz", + "integrity": "sha512-gKehi0BrEyZaRBNMzk6cywU9q3dfz5spKWndA4zuaYv4sFOfwVVaa4NfoHY1nPTd2/6xawLPSxO5JMxCZESuug==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-portal": "^9.4.7", + "@fluentui/react-positioning": "^9.11.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-popover/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-portal": { + "version": "9.4.7", + "resolved": "https://registry.npmjs.org/@fluentui/react-portal/-/react-portal-9.4.7.tgz", + "integrity": "sha512-4g0vvs+YKmBri24MBi+C3hkgKAEGhbNUi4+u1Oa0olrcGmbtugM0G910raDLJXLV2ssgSO8znzVRmA/4Lg7XYw==", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "use-disposable": "^1.0.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-portal/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-positioning": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-positioning/-/react-positioning-9.11.0.tgz", + "integrity": "sha512-kLtnAHpHkJLDIFctNfpDDbGBy5jw0n3ydGAvsiGVvcvBp4q0Ecy8d0ELLlwryVTxI8imdmeM/wkPOzz4LqS49A==", + "dependencies": { + "@floating-ui/devtools": "0.0.4", + "@floating-ui/dom": "^1.2.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-positioning/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-progress": { + "version": "9.1.57", + "resolved": "https://registry.npmjs.org/@fluentui/react-progress/-/react-progress-9.1.57.tgz", + "integrity": "sha512-QFBz9A9N4lyshev1b/r7OE5ea7/Q60Tav3zVGbfqVGmfA24qemmoDIYU1qWscuScRBmblR6//fvAroHhiK/FrQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-progress/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-provider": { + "version": "9.13.5", + "resolved": "https://registry.npmjs.org/@fluentui/react-provider/-/react-provider-9.13.5.tgz", + "integrity": "sha512-oxpGS8r5YEKHZy2csH9/kCJBgBMVms+HC0hiIA+J3M2iCdN+y1SuP0Y2zAYg/X/jRb1mXJzXtoWchsYvxVLnOQ==", + "dependencies": { + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/core": "^1.14.1", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-provider/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@emotion/memoize": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", - "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + "node_modules/@fluentui/react-radio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-radio/-/react-radio-9.2.0.tgz", + "integrity": "sha512-TowAnE7aMDq3fxCiouetjs8CN4KogfaarXYep3pXQMW7xRKBfXdOOFElYtQx/WzmikMJpDhhkZcyrqEpM5KGDQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } }, - "node_modules/@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" + "node_modules/@fluentui/react-radio/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@fluentui/react-select": { + "version": "9.1.57", + "resolved": "https://registry.npmjs.org/@fluentui/react-select/-/react-select-9.1.57.tgz", + "integrity": "sha512-QGlK+QYDiAzA6f3imGQBQOq9MQVVa88XyaSKWG82mjZfFWbs6vxaIZMXuCOTAsjcXKB4XDYVYOpman3apdLHjQ==", "dependencies": { - "eslint-visitor-keys": "^3.3.0" + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-select/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-shared-contexts": { + "version": "9.13.2", + "resolved": "https://registry.npmjs.org/@fluentui/react-shared-contexts/-/react-shared-contexts-9.13.2.tgz", + "integrity": "sha512-78aEZdff7vaUOmeRyMDPc/Ml+kbwn02BiRLPQhqgYtCyjy0V3YBpmYfqxO8N5hUIZcFTedyOaHWpzVeEYxpNmA==", + "dependencies": { + "@fluentui/react-theme": "^9.1.16", + "@swc/helpers": "^0.5.1" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.5.1.tgz", - "integrity": "sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node_modules/@fluentui/react-shared-contexts/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.0.3.tgz", - "integrity": "sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==", + "node_modules/@fluentui/react-skeleton": { + "version": "9.0.45", + "resolved": "https://registry.npmjs.org/@fluentui/react-skeleton/-/react-skeleton-9.0.45.tgz", + "integrity": "sha512-toOWQNoqONupLImvbkE4vf+ECq1lAyCUBf4hI6AOtYppDaXOOOWWsgSImsj25yLtS4dPYkzyWoMMdmjSOwXu6g==", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.5.2", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-skeleton/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-slider": { + "version": "9.1.62", + "resolved": "https://registry.npmjs.org/@fluentui/react-slider/-/react-slider-9.1.62.tgz", + "integrity": "sha512-GHY1J3qpoYY3c1rwQsBRTHISExO1vBSJCH4lDgCJI/E9KOa8saJYFvuv3aYmnOq8CGSb2UCSb0eha5G9UB/4yQ==", + "dependencies": { + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" }, - "funding": { - "url": "https://opencollective.com/eslint" + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" } }, - "node_modules/@eslint/js": { - "version": "8.40.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.40.0.tgz", - "integrity": "sha512-ElyB54bJIhXQYVKjDSvCkPO1iU1tSAeVQJbllWJq1XQSmmA4dgFk8CbiBGpiOPxleE48vDogxCtmMYku4HSVLA==", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node_modules/@fluentui/react-slider/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-spinbutton": { + "version": "9.2.57", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinbutton/-/react-spinbutton-9.2.57.tgz", + "integrity": "sha512-FoYe0iUcEYjlhJtqjciBdQ/4wuQ7iPwpqE/VuGknvWrydXawJ7qI3fx7yP/58R5utbGAz4Rdkkjzc2xhtW/EVg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-spinbutton/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-spinner": { + "version": "9.3.35", + "resolved": "https://registry.npmjs.org/@fluentui/react-spinner/-/react-spinner-9.3.35.tgz", + "integrity": "sha512-0JXgkVrB4+atN1P44XtuNup6xthg4gyJYPXGS+3x/EB2B0pi55c8IWE/mVjG9F/TBt2PFRb9nCzdrfvkhV/CsA==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-spinner/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-switch": { + "version": "9.1.62", + "resolved": "https://registry.npmjs.org/@fluentui/react-switch/-/react-switch-9.1.62.tgz", + "integrity": "sha512-L+MzTP3B5eWxw7az62Sf7FGzPhPQoD0CtyR78IHTrgPXQswnoSq9oJRkpVr5LBfND2SE4Y7UvPuUulBJXTlW5g==", + "dependencies": { + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-label": "^9.1.55", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-switch/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-table": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-table/-/react-table-9.11.0.tgz", + "integrity": "sha512-Hp7PBg0iy5W+y7j4QKIyzHd4dpdvIzcyvzYevTkJNWALyUzzZZ6yefLS+hHy/akP/gEOGctnNyJENiODBSSbAA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-avatar": "^9.6.4", + "@fluentui/react-checkbox": "^9.2.5", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-radio": "^9.2.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-table/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-tabs": { + "version": "9.4.3", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabs/-/react-tabs-9.4.3.tgz", + "integrity": "sha512-H/bM6NfA3y81NdOMmCWMhpv4SDAWkxMq8ywx8Pr98pKvkearlIo9EuxFK84YEK0haibgn6OpWSPciVaBLx8dPw==", + "dependencies": { + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0", + "scheduler": "^0.19.0 || ^0.20.0" + } + }, + "node_modules/@fluentui/react-tabs/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-tabster": { + "version": "9.17.0", + "resolved": "https://registry.npmjs.org/@fluentui/react-tabster/-/react-tabster-9.17.0.tgz", + "integrity": "sha512-+tFYkjH5QiF2YDCmJAz1FVzzs/09hiRT0xPe1yLlAkMYJV/s+iIungwKgSUF1RziP9iQh9NZZ2A5GMrzRw8SGg==", + "dependencies": { + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "keyborg": "^2.3.0", + "tabster": "^5.0.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tabster/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-tags": { + "version": "9.0.17", + "resolved": "https://registry.npmjs.org/@fluentui/react-tags/-/react-tags-9.0.17.tgz", + "integrity": "sha512-XV3mtMgPcbXwDlrXVU19hfW0MsXmhM+ZHS0oZgR/jSDO6Nses6jdS3qiG2+Nl/AJqW6wA2l2yCBXHdAZu7hC8A==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-avatar": "^9.6.4", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tags/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-text": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/@fluentui/react-text/-/react-text-9.4.4.tgz", + "integrity": "sha512-FH/zua+u+T8QK1cDeg4w1Ahdfj+2A3Wd61g9lxU26ZfUzhWxV7F9enwGgiqTF0Swv7WsD1ahwI+FIZA5+WgEsw==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-text/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-textarea": { + "version": "9.3.57", + "resolved": "https://registry.npmjs.org/@fluentui/react-textarea/-/react-textarea-9.3.57.tgz", + "integrity": "sha512-E26jQ1s7/QLkJjTw72BQHfKtXP8PyNsfPSfWbwhmm5doe0HLa1V5+Gq1ZkdQiV78Q5Rl0bGKXrdR2Ccy/fBL/g==", + "dependencies": { + "@fluentui/react-field": "^9.1.47", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-textarea/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-theme": { + "version": "9.1.16", + "resolved": "https://registry.npmjs.org/@fluentui/react-theme/-/react-theme-9.1.16.tgz", + "integrity": "sha512-QK2dGE5aQXN1UGdiEmGKpYGP3tHXIchLvFf8DEEOWnF4XBc9SiEPNFYkvLMJjHxZmDz4D670rsOPe0r5jFDEKQ==", + "dependencies": { + "@fluentui/tokens": "1.0.0-alpha.13", + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/react-theme/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-toast": { + "version": "9.3.23", + "resolved": "https://registry.npmjs.org/@fluentui/react-toast/-/react-toast-9.3.23.tgz", + "integrity": "sha512-PJClzPYWmq303SoaNuPwgF0Drbn0pGrzqA6x8J3NgCx7F4c8n5SB34jxKtKlFAB5EiAk1y+v1om7Z6/v+3Zehg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-portal": "^9.4.7", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1", + "react-transition-group": "^4.4.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-toast/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-toolbar": { + "version": "9.1.62", + "resolved": "https://registry.npmjs.org/@fluentui/react-toolbar/-/react-toolbar-9.1.62.tgz", + "integrity": "sha512-97mlwQsAXn5jYhvyUUU7FrMIwddN5CTQEpoTDOaibcZ1hvZTNyVDyLpvWy7CLjX8CPvde/jgjf4z+I74zYP0OA==", + "dependencies": { + "@fluentui/react-button": "^9.3.61", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-divider": "^9.2.55", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-radio": "^9.2.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-toolbar/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-tooltip": { + "version": "9.4.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-tooltip/-/react-tooltip-9.4.6.tgz", + "integrity": "sha512-G3u6Qdr8mBClcncmf4qzIiPx4ti1ZjhB/lFVYIyB8Egg6s20TBRajo3ZlTD+Wwo9In8szfLm836bz8SMZqL4KA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-portal": "^9.4.7", + "@fluentui/react-positioning": "^9.11.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tooltip/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-tree": { + "version": "9.4.20", + "resolved": "https://registry.npmjs.org/@fluentui/react-tree/-/react-tree-9.4.20.tgz", + "integrity": "sha512-LTzGiJeKCXYHUB7gY/XiQcowTVirz402Oeb6As8WM99pmWAzmttzSTiS7+MeHqJEeisNzOojyR8bcqRJ3DY7Pg==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-aria": "^9.6.2", + "@fluentui/react-avatar": "^9.6.4", + "@fluentui/react-button": "^9.3.61", + "@fluentui/react-checkbox": "^9.2.5", + "@fluentui/react-context-selector": "^9.1.46", + "@fluentui/react-icons": "^2.0.224", + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-radio": "^9.2.0", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-tabster": "^9.17.0", + "@fluentui/react-theme": "^9.1.16", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-tree/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-utilities": { + "version": "9.15.6", + "resolved": "https://registry.npmjs.org/@fluentui/react-utilities/-/react-utilities-9.15.6.tgz", + "integrity": "sha512-Hli0iiA/gaWwADMe7NRD6TSy7KvL3bgek8j1sYkE9BiUI89GqyfJwU2Tm0it04iiCYvQ5WWrXPcRYyZ3/MHtpA==", + "dependencies": { + "@fluentui/keyboard-keys": "^9.0.7", + "@fluentui/react-shared-contexts": "^9.13.2", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-utilities/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/react-virtualizer": { + "version": "9.0.0-alpha.63", + "resolved": "https://registry.npmjs.org/@fluentui/react-virtualizer/-/react-virtualizer-9.0.0-alpha.63.tgz", + "integrity": "sha512-Uddb1bIayzXwSEJg90ybNUa1NE84aOCFRKAf1E2by7mSQupyOk3NjrVSpm3O78vUdPyjJN+COZlTsEFE3EBbIg==", + "dependencies": { + "@fluentui/react-jsx-runtime": "^9.0.24", + "@fluentui/react-shared-contexts": "^9.13.2", + "@fluentui/react-utilities": "^9.15.6", + "@griffel/react": "^1.5.14", + "@swc/helpers": "^0.5.1" + }, + "peerDependencies": { + "@types/react": ">=16.14.0 <19.0.0", + "@types/react-dom": ">=16.14.0 <19.0.0", + "react": ">=16.14.0 <19.0.0", + "react-dom": ">=16.14.0 <19.0.0" + } + }, + "node_modules/@fluentui/react-virtualizer/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@fluentui/tokens": { + "version": "1.0.0-alpha.13", + "resolved": "https://registry.npmjs.org/@fluentui/tokens/-/tokens-1.0.0-alpha.13.tgz", + "integrity": "sha512-IzYysTTBkAH7tQZxYKpzhxYnTJkvwXhjhTOpmERgnqTFifHTP8/vaQjJAAm7dI/9zlDx1oN+y/I+KzL9bDLHZQ==", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/tokens/node_modules/@swc/helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", + "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@griffel/core": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@griffel/core/-/core-1.15.1.tgz", + "integrity": "sha512-09w5axbOJuBzjTVFN5EycbAOIpCoxZeoJMZgT2fTrIl2GIxnTWpK3F2d63UzGBmQf1O+EgR6nK4FgMXFt1UFig==", + "dependencies": { + "@emotion/hash": "^0.9.0", + "@griffel/style-types": "^1.0.2", + "csstype": "^3.1.2", + "rtl-css-js": "^1.16.1", + "stylis": "^4.2.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@griffel/react": { + "version": "1.5.19", + "resolved": "https://registry.npmjs.org/@griffel/react/-/react-1.5.19.tgz", + "integrity": "sha512-qefnZseAwcwCpFVzN33mG20t/hofpWci7VNtOwzSco/IxFLuJaB2ffki+uAdUgWCpV2A67bWQjXNlymBoMUysg==", + "dependencies": { + "@griffel/core": "^1.15.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.8.0 <19.0.0" + } + }, + "node_modules/@griffel/style-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@griffel/style-types/-/style-types-1.0.2.tgz", + "integrity": "sha512-ka/Tpl1WU8js88LObwB/4EvpgXzx/EEJfbHhAr4ZNt29hrQKgL93X1zSY6M/FRhMhWrGIawauWkZP6/y6w/WiQ==", + "dependencies": { + "csstype": "^3.1.2" } }, "node_modules/@humanwhocodes/config-array": { @@ -12961,6 +14705,11 @@ "node": ">=4.0" } }, + "node_modules/keyborg": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/keyborg/-/keyborg-2.4.0.tgz", + "integrity": "sha512-EixFnyCc6m27NkFwyT5GNNQl+9wiTMePtAvVuKXZpc0CZLLJfJFB3zXGFBGnwtvEXymbJoPWoErzwTpiEQ+Msg==" + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -16791,6 +18540,14 @@ "react-dom": "^18.2.0" } }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -17574,6 +19331,11 @@ "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.1.3.tgz", "integrity": "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA==" }, + "node_modules/stylis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz", + "integrity": "sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==" + }, "node_modules/sucrase": { "version": "3.32.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.32.0.tgz", @@ -17841,6 +19603,15 @@ "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" }, + "node_modules/tabster": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/tabster/-/tabster-5.2.0.tgz", + "integrity": "sha512-cSi3a0gGeM9Co/gTKHlhTFfiitwVjcA+kP9lJux0U7QaRrZox1yYrfbwZhJXM7N0fux7BgvCYaOxME5k0EQ0tA==", + "dependencies": { + "keyborg": "^2.2.0", + "tslib": "^2.3.1" + } + }, "node_modules/tailwindcss": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.2.tgz", @@ -18557,6 +20328,17 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-disposable": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/use-disposable/-/use-disposable-1.0.2.tgz", + "integrity": "sha512-UMaXVlV77dWOu4GqAFNjRzHzowYKUKbJBQfCexvahrYeIz4OkUYUjna4Tjjdf92NH8Nm8J7wEfFRgTIwYjO5jg==", + "peerDependencies": { + "@types/react": ">=16.8.0 <19.0.0", + "@types/react-dom": ">=16.8.0 <19.0.0", + "react": ">=16.8.0 <19.0.0", + "react-dom": ">=16.8.0 <19.0.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz", diff --git a/feedingwebapp/package.json b/feedingwebapp/package.json index d1935619..93b1d99d 100644 --- a/feedingwebapp/package.json +++ b/feedingwebapp/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fluentui/react-components": "^9.44.1", "@mapbox/node-pre-gyp": "^1.0.11", "@testing-library/jest-dom": "^5.16.4", "@testing-library/react": "^13.2.0", diff --git a/feedingwebapp/src/Pages/Constants.js b/feedingwebapp/src/Pages/Constants.js index 39fe6386..ab82a840 100644 --- a/feedingwebapp/src/Pages/Constants.js +++ b/feedingwebapp/src/Pages/Constants.js @@ -23,11 +23,9 @@ export const TIME_TO_RESET_MS = 3600000 // 1 hour in milliseconds */ let MOVING_STATE_ICON_DICT = {} MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingAbovePlate] = '/robot_state_imgs/move_above_plate_position.svg' -MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToAbovePlate] = '/robot_state_imgs/move_above_plate_position.svg' MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToRestingPosition] = '/robot_state_imgs/move_to_resting_position.svg' -MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToRestingPosition] = '/robot_state_imgs/move_to_resting_position.svg' MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToStagingConfiguration] = '/robot_state_imgs/move_to_staging_configuration.svg' -MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToStagingConfiguration] = '/robot_state_imgs/move_to_staging_configuration.svg' +MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouth] = '/robot_state_imgs/move_to_staging_configuration.svg' MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToMouth] = '/robot_state_imgs/move_to_mouth_position.svg' MOVING_STATE_ICON_DICT[MEAL_STATE.R_StowingArm] = '/robot_state_imgs/stowing_arm_position.svg' export { MOVING_STATE_ICON_DICT } @@ -69,10 +67,6 @@ ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingAbovePlate] = { actionName: 'MoveAbovePlate', messageType: 'ada_feeding_msgs/action/MoveTo' } -ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToAbovePlate] = { - actionName: 'MoveFromMouthToAbovePlate', - messageType: 'ada_feeding_msgs/action/MoveTo' -} ROS_ACTIONS_NAMES[MEAL_STATE.U_BiteSelection] = { actionName: 'SegmentFromPoint', messageType: 'ada_feeding_msgs/action/SegmentFromPoint' @@ -85,16 +79,12 @@ ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToRestingPosition] = { actionName: 'MoveToRestingPosition', messageType: 'ada_feeding_msgs/action/MoveTo' } -ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToRestingPosition] = { - actionName: 'MoveFromMouthToRestingPosition', - messageType: 'ada_feeding_msgs/action/MoveTo' -} ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToStagingConfiguration] = { actionName: 'MoveToStagingConfiguration', messageType: 'ada_feeding_msgs/action/MoveTo' } -ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouthToStagingConfiguration] = { - actionName: 'MoveFromMouthToStagingConfiguration', +ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingFromMouth] = { + actionName: 'MoveFromMouth', messageType: 'ada_feeding_msgs/action/MoveTo' } ROS_ACTIONS_NAMES[MEAL_STATE.R_MovingToMouth] = { @@ -119,6 +109,10 @@ ROS_SERVICE_NAMES[MEAL_STATE.R_DetectingFace] = { export { ROS_SERVICE_NAMES } export const CLEAR_OCTOMAP_SERVICE_NAME = 'clear_octomap' export const CLEAR_OCTOMAP_SERVICE_TYPE = 'std_srvs/srv/Empty' +export const GET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/get_parameters' +export const GET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/GetParameters' +export const SET_PARAMETERS_SERVICE_NAME = 'ada_feeding_action_servers/set_parameters' +export const SET_PARAMETERS_SERVICE_TYPE = 'rcl_interfaces/srv/SetParameters' /** * The meaning of the status that motion actions return in their results. diff --git a/feedingwebapp/src/Pages/Footer/Footer.jsx b/feedingwebapp/src/Pages/Footer/Footer.jsx index ff2b6ea7..c80eeb0a 100644 --- a/feedingwebapp/src/Pages/Footer/Footer.jsx +++ b/feedingwebapp/src/Pages/Footer/Footer.jsx @@ -9,13 +9,13 @@ import { useMediaQuery } from 'react-responsive' import PropTypes from 'prop-types' // Local imports import { MOVING_STATE_ICON_DICT } from '../Constants' -import { useGlobalState } from '../GlobalState' /** * The Footer shows a pause button. When users click it, the app tells the robot * to immediately pause and displays a back button that allows them to return to * previous state and a resume button that allows them to resume current state. * + * @param {string} mealState - the current meal state * @param {bool} paused - whether the robot is currently paused * @param {function} pauseCallback - callback function for when the pause button * is clicked @@ -27,14 +27,12 @@ import { useGlobalState } from '../GlobalState' * button is clicked. If null, don't render the resume button. */ const Footer = (props) => { - // Get the current meal state - const mealState = useGlobalState((state) => state.mealState) // Flag to check if the current orientation is portrait const isPortrait = useMediaQuery({ query: '(orientation: portrait)' }) // Icons for the footer buttons let pauseIcon = '/robot_state_imgs/pause_button_icon.svg' let backIcon = props.backMealState ? MOVING_STATE_ICON_DICT[props.backMealState] : '' - let resumeIcon = MOVING_STATE_ICON_DICT[mealState] + let resumeIcon = MOVING_STATE_ICON_DICT[props.mealState] // Sizes (width, height, fontsize) of footer buttons let pauseButtonWidth = '98vw' let backResumeButtonWidth = '47vw' @@ -95,7 +93,7 @@ const Footer = (props) => { (config) => { return ( <> - <Row className='justify-content-center'> + <Row className='justify-content-center' style={{ width: '100%' }}> <Button variant={config.variant} disabled={config.disabled} @@ -163,34 +161,37 @@ const Footer = (props) => { // Render the component return ( - <View> + <View style={{ wdith: '100%' }}> <MDBFooter bgColor='dark' className='text-center text-lg-left' style={{ width: '100vw' }}> <div className='text-center' style={{ backgroundColor: 'rgba(0, 0, 0, 0.2)', paddingBottom: '5px', paddingTop: '5px' }}> - {props.paused ? ( - <View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%' }}> - <View - style={{ flex: 5, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }} - > - {props.backCallback ? renderFooterButton(buttonConfig.back) : <></>} - </View> - <View - style={{ flex: 5, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }} - > - {props.resumeCallback ? renderFooterButton(buttonConfig.resume) : <></>} - </View> - </View> - ) : ( - renderFooterButton(buttonConfig.pause) - )} + <View style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%' }}> + {props.paused ? ( + <> + <View + style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }} + > + {props.backCallback ? renderFooterButton(buttonConfig.back) : <></>} + </View> + <View + style={{ flex: 1, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', width: '100%', height: '100%' }} + > + {props.resumeCallback ? renderFooterButton(buttonConfig.resume) : <></>} + </View> + </> + ) : ( + renderFooterButton(buttonConfig.pause) + )} + </View> </div> </MDBFooter> </View> ) } Footer.propTypes = { + mealState: PropTypes.string.isRequired, paused: PropTypes.bool.isRequired, pauseCallback: PropTypes.func.isRequired, - // If any of the below three are null, the Footer won't render that button + // If any of the below two are null, the Footer won't render that button resumeCallback: PropTypes.func, backCallback: PropTypes.func, backMealState: PropTypes.string diff --git a/feedingwebapp/src/Pages/GlobalState.jsx b/feedingwebapp/src/Pages/GlobalState.jsx index 76de930d..892a626f 100644 --- a/feedingwebapp/src/Pages/GlobalState.jsx +++ b/feedingwebapp/src/Pages/GlobalState.jsx @@ -39,18 +39,9 @@ export const APP_PAGE = { * - R_DetectingFace: Waiting for the robot to detect a face. * - R_MovingToMouth: Waiting for the robot to finish moving to the user's * mouth. - * - R_MovingFromMouthToStagingConfiguration: Waiting for the robot to move + * - R_MovingFromMouth: Waiting for the robot to move * from the user's mouth to the staging configuration. This is a separate - * action from R_MovingToStagingConfiguration to allow us to customize the - * departure from the mouth (e.g., a slower speed). - * - R_MovingFromMouthToAbovePlate: Waiting for the robot to move from the - * user's mouth to above the plate. This is a separate action from - * R_MovingAbovePlate to allow us to customize the departure from the mouth - * (e.g., a slower speed). - * - R_MovingFromMouthToRestingPosition: Waiting for the robot to move from - * the user's mouth to resting position. This is a separate action from - * R_MovingToRestingPosition to allow us to customize the departure from - * the mouth (e.g., a slower speed). + * action from R_MovingToStagingConfiguration since it is cartesian. * - U_BiteDone: Waiting for the user to indicate that they are done eating * the bite. * - R_StowingArm: Waiting for the robot to stow the arm. @@ -67,14 +58,23 @@ export const MEAL_STATE = { R_MovingToStagingConfiguration: 'R_MovingToStagingConfiguration', R_DetectingFace: 'R_DetectingFace', R_MovingToMouth: 'R_MovingToMouth', - R_MovingFromMouthToStagingConfiguration: 'R_MovingFromMouthToStagingConfiguration', - R_MovingFromMouthToAbovePlate: 'R_MovingFromMouthToAbovePlate', - R_MovingFromMouthToRestingPosition: 'R_MovingFromMouthToRestingPosition', + R_MovingFromMouth: 'R_MovingFromMouth', U_BiteDone: 'U_BiteDone', R_StowingArm: 'R_StowingArm', U_PostMeal: 'U_PostMeal' } +/** + * SETTINGS_STATE controls which settings page to display. + * - MAIN: The main page, with options to navigate to the other pages. + * - BITE_TRANSFER: The bite transfer page, where the user can configure + * parameters for bite transfer. + */ +export const SETTINGS_STATE = { + MAIN: 'MAIN', + BITE_TRANSFER: 'BITE_TRANSFER' +} + /** * The parameters that users can set (keys) and a list of human-readable values * they can take on. @@ -90,11 +90,11 @@ export const MEAL_STATE = { * TODO (amaln): When we connect this to ROS, each of these settings types and * value options will have to have corresponding rosparam names and value options. */ -export const SETTINGS = { - stagingPosition: ['In Front of Me', 'On My Right Side'], - biteInitiation: ['Open Mouth', 'Say "I am Ready"', 'Press Button'], - biteSelection: ['Name of Food', 'Click on Food'] -} +// export const SETTINGS = { +// stagingPosition: ['In Front of Me', 'On My Right Side'], +// biteInitiation: ['Open Mouth', 'Say "I am Ready"', 'Press Button'], +// biteSelection: ['Name of Food', 'Click on Food'] +// } /** * useGlobalState is a hook to store and manipulate web app state that we want @@ -104,12 +104,14 @@ export const SETTINGS = { export const useGlobalState = create( persist( (set) => ({ + // The current app page + appPage: APP_PAGE.Home, // The app's current meal state mealState: MEAL_STATE.U_PreMeal, // The timestamp when the robot transitioned to its current meal state mealStateTransitionTime: Date.now(), - // The current app page - appPage: APP_PAGE.Home, + // The currently displayed settings page + settingsState: SETTINGS_STATE.MAIN, // The goal for the bite acquisition action, including the most recent // food item that the user selected in "bite selection" biteAcquisitionActionGoal: null, @@ -123,20 +125,44 @@ export const useGlobalState = create( teleopIsMoving: false, // Flag to indicate whether to auto-continue after face detection faceDetectionAutoContinue: false, + // Whether the settings bite transfer page is currently at the user's face + // or not. This is in the off-chance that the mealState is not at the user's + // face, the settings page is, and the user refreshes -- the page should + // call MoveFromMouthToStaging instead of just MoveToStaging. + biteTransferPageAtFace: false, + // The button the user most recently clicked on the BiteDone page. In practice, + // this is the state we transition to after R_MovingFromMouth. In practice, + // it is either R_MovingAbovePlate, R_MovingToRestingPosition, or R_DetectingFace. + mostRecentBiteDoneResponse: MEAL_STATE.R_DetectingFace, // Settings values - stagingPosition: SETTINGS.stagingPosition[0], - biteInitiation: SETTINGS.biteInitiation[0], - biteSelection: SETTINGS.biteSelection[0], + // stagingPosition: SETTINGS.stagingPosition[0], + // biteInitiation: SETTINGS.biteInitiation[0], + // biteSelection: SETTINGS.biteSelection[0], // Setters for global state - setMealState: (mealState) => + setAppPage: (appPage) => set(() => ({ - mealState: mealState, - mealStateTransitionTime: Date.now() + appPage: appPage, + settingsState: SETTINGS_STATE.MAIN, + // Sometimes the settings menu leaves the robot in a paused state. + // Thus, we reset it to an unpaused state. + paused: false })), - setAppPage: (appPage) => + setMealState: (mealState, mostRecentBiteDoneResponse = null) => + set(() => { + let retval = { + mealState: mealState, + mealStateTransitionTime: Date.now(), + biteTransferPageAtFace: false // Reset this flag when the meal state changes + } + if (mostRecentBiteDoneResponse) { + retval.mostRecentBiteDoneResponse = mostRecentBiteDoneResponse + } + return retval + }), + setSettingsState: (settingsState) => set(() => ({ - appPage: appPage + settingsState: settingsState })), setBiteAcquisitionActionGoal: (biteAcquisitionActionGoal) => set(() => ({ @@ -158,18 +184,22 @@ export const useGlobalState = create( set(() => ({ faceDetectionAutoContinue: faceDetectionAutoContinue })), - setStagingPosition: (stagingPosition) => - set(() => ({ - stagingPosition: stagingPosition - })), - setBiteInitiation: (biteInitiation) => - set(() => ({ - biteInitiation: biteInitiation - })), - setBiteSelection: (biteSelection) => + setBiteTransferPageAtFace: (biteTransferPageAtFace) => set(() => ({ - biteSelection: biteSelection + biteTransferPageAtFace: biteTransferPageAtFace })) + // setStagingPosition: (stagingPosition) => + // set(() => ({ + // stagingPosition: stagingPosition + // })), + // setBiteInitiation: (biteInitiation) => + // set(() => ({ + // biteInitiation: biteInitiation + // })), + // setBiteSelection: (biteSelection) => + // set(() => ({ + // biteSelection: biteSelection + // })) }), { name: 'ada_web_app_global_state' } ) diff --git a/feedingwebapp/src/Pages/Header/Header.jsx b/feedingwebapp/src/Pages/Header/Header.jsx index 9b8ae112..2525584c 100644 --- a/feedingwebapp/src/Pages/Header/Header.jsx +++ b/feedingwebapp/src/Pages/Header/Header.jsx @@ -6,7 +6,7 @@ import Navbar from 'react-bootstrap/Navbar' import Nav from 'react-bootstrap/Nav' import { useMediaQuery } from 'react-responsive' // Toast generates a temporary pop-up with a timeout. -import { ToastContainer /* , toast */ } from 'react-toastify' +import { ToastContainer, toast } from 'react-toastify' import 'react-toastify/dist/ReactToastify.css' // ROS imports import { useROS } from '../../ros/ros_helpers' @@ -63,13 +63,13 @@ const Header = (props) => { * started, take the user to the settings menu. Else, ask them to complete * or terminate the meal because modifying settings. */ - // const settingsClicked = useCallback(() => { - // if (mealState === MEAL_STATE.U_PreMeal || mealState === MEAL_STATE.U_PostMeal) { - // setAppPage(APP_PAGE.Settings) - // } else { - // toast('Please complete or terminate the feeding process to access Settings.') - // } - // }, [mealState, setAppPage]) + const settingsClicked = useCallback(() => { + if (NON_MOVING_STATES.has(mealState)) { + setAppPage(APP_PAGE.Settings) + } else { + toast('Wait for robot motion to complete before accessing Settings.') + } + }, [mealState, setAppPage]) // Render the component. The NavBar will stay fixed even as we vertically scroll. return ( @@ -107,14 +107,13 @@ const Header = (props) => { > Home </Nav.Link> - {/* TODO: Reinstate the settings menu when we implement settings! */} - {/* <Nav.Link + <Nav.Link onClick={settingsClicked} className='text-dark bg-info rounded mx-1 btn-lg btn-huge p-2' style={{ fontSize: textFontSize }} > Settings - </Nav.Link> */} + </Nav.Link> </Nav> {NON_MOVING_STATES.has(mealState) || paused || (mealState === MEAL_STATE.U_PlateLocator && teleopIsMoving === false) ? ( <Nav> diff --git a/feedingwebapp/src/Pages/Home/Home.jsx b/feedingwebapp/src/Pages/Home/Home.jsx index 8578eec4..58431fde 100644 --- a/feedingwebapp/src/Pages/Home/Home.jsx +++ b/feedingwebapp/src/Pages/Home/Home.jsx @@ -32,6 +32,24 @@ function Home(props) { const setMoveToMouthActionGoal = useGlobalState((state) => state.setMoveToMouthActionGoal) const setMealState = useGlobalState((state) => state.setMealState) const setPaused = useGlobalState((state) => state.setPaused) + const biteAcquisitionActionGoal = useGlobalState((state) => state.biteAcquisitionActionGoal) + const moveToMouthActionGoal = useGlobalState((state) => state.moveToMouthActionGoal) + const mostRecentBiteDoneResponse = useGlobalState((state) => state.mostRecentBiteDoneResponse) + + // Implement a wrapper around setMealState, that resets mostRecentBiteDoneResponse to + // R_DetectingFace if the mealState is mostRecentBiteDoneResponse. In other words, + // after you transition to that state once, you need to revisit BiteDone to transition + // again. + const setMealStateWrapper = useCallback( + (newMealState) => { + if (newMealState === mostRecentBiteDoneResponse) { + setMealState(newMealState, MEAL_STATE.R_DetectingFace) + } else { + setMealState(newMealState) + } + }, + [mostRecentBiteDoneResponse, setMealState] + ) /** * Implement time-based transition of states. This is so that after the user @@ -49,10 +67,6 @@ function Home(props) { } }, [mealStateTransitionTime, setMealState, setPaused, setMoveToMouthActionGoal, setBiteAcquisitionActionGoal]) - // Get the relevant global variables - const biteAcquisitionActionGoal = useGlobalState((state) => state.biteAcquisitionActionGoal) - const moveToMouthActionGoal = useGlobalState((state) => state.moveToMouthActionGoal) - /** * All action inputs are constant. Note that we must be cautious if making * them non-constant, because the robot will re-execute an action every time @@ -81,12 +95,15 @@ function Home(props) { */ let currentMealState = MEAL_STATE.R_MovingAbovePlate let nextMealState = MEAL_STATE.U_BiteSelection + let backMealState = null let waitingText = 'Waiting to move above the plate...' return ( <RobotMotion debug={props.debug} mealState={currentMealState} + setMealState={setMealStateWrapper} nextMealState={nextMealState} + backMealState={backMealState} actionInput={moveAbovePlateActionInput} waitingText={waitingText} /> @@ -105,12 +122,15 @@ function Home(props) { */ let currentMealState = MEAL_STATE.R_BiteAcquisition let nextMealState = MEAL_STATE.U_BiteAcquisitionCheck + let backMealState = MEAL_STATE.R_MovingAbovePlate let waitingText = 'Waiting to acquire the food...' return ( <RobotMotion debug={props.debug} mealState={currentMealState} + setMealState={setMealStateWrapper} nextMealState={nextMealState} + backMealState={backMealState} actionInput={biteAcquisitionActionInput} waitingText={waitingText} /> @@ -119,12 +139,15 @@ function Home(props) { case MEAL_STATE.R_MovingToRestingPosition: { let currentMealState = MEAL_STATE.R_MovingToRestingPosition let nextMealState = MEAL_STATE.U_BiteAcquisitionCheck + let backMealState = MEAL_STATE.R_MovingAbovePlate let waitingText = 'Waiting to move to the resting position...' return ( <RobotMotion debug={props.debug} mealState={currentMealState} + setMealState={setMealStateWrapper} nextMealState={nextMealState} + backMealState={backMealState} actionInput={moveToRestingPositionActionInput} waitingText={waitingText} /> @@ -140,12 +163,15 @@ function Home(props) { */ let currentMealState = MEAL_STATE.R_MovingToStagingConfiguration let nextMealState = MEAL_STATE.R_DetectingFace + let backMealState = MEAL_STATE.R_MovingToRestingPosition let waitingText = 'Waiting to move in front of you...' return ( <RobotMotion debug={props.debug} mealState={currentMealState} + setMealState={setMealStateWrapper} nextMealState={nextMealState} + backMealState={backMealState} actionInput={moveToStagingConfigurationActionInput} waitingText={waitingText} /> @@ -161,67 +187,44 @@ function Home(props) { */ let currentMealState = MEAL_STATE.R_MovingToMouth let nextMealState = MEAL_STATE.U_BiteDone + let backMealState = MEAL_STATE.R_MovingFromMouth let waitingText = 'Waiting to move to your mouth...' return ( <RobotMotion debug={props.debug} mealState={currentMealState} + setMealState={setMealStateWrapper} nextMealState={nextMealState} + backMealState={backMealState} actionInput={moveToMouthActionInput} waitingText={waitingText} /> ) } - case MEAL_STATE.R_MovingFromMouthToStagingConfiguration: { + case MEAL_STATE.R_MovingFromMouth: { /** * We recreate currentMealState due to a race condition where sometimes * the app is performing a re-rendering and *then* the state is updated. */ - let currentMealState = MEAL_STATE.R_MovingFromMouthToStagingConfiguration - let nextMealState = MEAL_STATE.R_DetectingFace - let waitingText = 'Waiting to move from your mouth to in front of you...' + let currentMealState = MEAL_STATE.R_MovingFromMouth + let nextMealState = mostRecentBiteDoneResponse + // Although slightly unintuitive, having backMealState being MovingAbovePlate + // is necessary so the user doesn't get stuck in a situation where they can't + // move above the plate. + let backMealState = MEAL_STATE.R_MovingAbovePlate + let waitingText = 'Waiting to away from your mouth...' return ( <RobotMotion debug={props.debug} mealState={currentMealState} + setMealState={setMealStateWrapper} nextMealState={nextMealState} + backMealState={backMealState} actionInput={moveToStagingConfigurationActionInput} waitingText={waitingText} /> ) } - case MEAL_STATE.R_MovingFromMouthToAbovePlate: { - /** - * We recreate currentMealState due to a race condition where sometimes - * the app is performing a re-rendering and *then* the state is updated. - */ - let currentMealState = MEAL_STATE.R_MovingFromMouthToAbovePlate - let nextMealState = MEAL_STATE.U_BiteSelection - let waitingText = 'Waiting to move from your mouth to above the plate...' - return ( - <RobotMotion - debug={props.debug} - mealState={currentMealState} - nextMealState={nextMealState} - actionInput={moveAbovePlateActionInput} - waitingText={waitingText} - /> - ) - } - case MEAL_STATE.R_MovingFromMouthToRestingPosition: { - let currentMealState = MEAL_STATE.R_MovingFromMouthToRestingPosition - let nextMealState = MEAL_STATE.U_BiteAcquisitionCheck - let waitingText = 'Waiting to move from your mouth to the resting position...' - return ( - <RobotMotion - debug={props.debug} - mealState={currentMealState} - nextMealState={nextMealState} - actionInput={moveToRestingPositionActionInput} - waitingText={waitingText} - /> - ) - } case MEAL_STATE.U_BiteDone: { return <BiteDone debug={props.debug} /> } @@ -232,12 +235,15 @@ function Home(props) { */ let currentMealState = MEAL_STATE.R_StowingArm let nextMealState = MEAL_STATE.U_PostMeal + let backMealState = MEAL_STATE.R_MovingAbovePlate let waitingText = 'Waiting to get out of your way...' return ( <RobotMotion debug={props.debug} mealState={currentMealState} + setMealState={setMealStateWrapper} nextMealState={nextMealState} + backMealState={backMealState} actionInput={moveToStowPositionActionInput} waitingText={waitingText} /> @@ -252,9 +258,11 @@ function Home(props) { } }, [ mealState, + setMealStateWrapper, props.debug, props.webrtcURL, biteAcquisitionActionInput, + mostRecentBiteDoneResponse, moveAbovePlateActionInput, moveToMouthActionInput, moveToRestingPositionActionInput, diff --git a/feedingwebapp/src/Pages/Home/MealStates/BiteDone.jsx b/feedingwebapp/src/Pages/Home/MealStates/BiteDone.jsx index 5332d3ea..b2538deb 100644 --- a/feedingwebapp/src/Pages/Home/MealStates/BiteDone.jsx +++ b/feedingwebapp/src/Pages/Home/MealStates/BiteDone.jsx @@ -18,15 +18,17 @@ const BiteDone = () => { // Get the relevant global variables const setMealState = useGlobalState((state) => state.setMealState) // Get icon image for move above plate - let moveAbovePlateImage = MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToAbovePlate] + let moveAbovePlateImage = MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingAbovePlate] // Get icon image for move to resting position - let moveToRestingPositionImage = MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingFromMouthToRestingPosition] + let moveToRestingPositionImage = MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToRestingPosition] + // Get icom image for move to staging configuration + let moveToStagingConfigurationImage = MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToStagingConfiguration] // Flag to check if the current orientation is portrait const isPortrait = useMediaQuery({ query: '(orientation: portrait)' }) // Indicator of how to arrange screen elements based on orientation let dimension = isPortrait ? 'column' : 'row' // Font size for text - let textFontSize = isPortrait ? '3vh' : '3vw' + let textFontSize = isPortrait ? '3vh' : '2.5vw' let buttonWidth = isPortrait ? '30vh' : '30vw' let buttonHeight = isPortrait ? '20vh' : '20vw' let iconWidth = isPortrait ? '28vh' : '28vw' @@ -36,96 +38,22 @@ const BiteDone = () => { * Callback function for when the user wants to move above plate. */ const moveAbovePlate = useCallback(() => { - setMealState(MEAL_STATE.R_MovingFromMouthToAbovePlate) + setMealState(MEAL_STATE.R_MovingFromMouth, MEAL_STATE.R_MovingAbovePlate) }, [setMealState]) /** * Callback function for when the user wants to move to resting position. */ const moveToRestingPosition = useCallback(() => { - setMealState(MEAL_STATE.R_MovingFromMouthToRestingPosition) + setMealState(MEAL_STATE.R_MovingFromMouth, MEAL_STATE.R_MovingToRestingPosition) }, [setMealState]) /** - * Get the bite finished text to render. - * - * @returns {JSX.Element} the bite finished text - */ - const biteFinishedText = useCallback(() => { - return ( - <> - {/* Ask the user whether they want to move to above plate position */} - <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize }}> - Bite finished? Move above plate. - </p> - </> - ) - }, [textFontSize]) - - /** - * Get the bite finished button to render. - * - * @returns {JSX.Element} the bite finished button + * Callback function for when the user wants to move to the staging configuration. */ - const biteFinishedButton = useCallback(() => { - return ( - <> - {/* Icon to move above plate */} - <Button - variant='success' - className='mx-2 mb-2 btn-huge' - size='lg' - onClick={moveAbovePlate} - style={{ width: buttonWidth, height: buttonHeight }} - > - <img src={moveAbovePlateImage} alt='move_above_plate_image' className='center' style={{ width: iconWidth, height: iconHeight }} /> - </Button> - </> - ) - }, [moveAbovePlate, moveAbovePlateImage, buttonHeight, buttonWidth, iconHeight, iconWidth]) - - /** - * Get the take another bite text to render. - * - * @returns {JSX.Element} the take another bite text - */ - const takeAnotherBiteText = useCallback(() => { - return ( - <> - {/* Ask the user whether they want to move to resting position */} - <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize }}> - Take another bite? Move back. - </p> - </> - ) - }, [textFontSize]) - - /** - * Get the take another bite button to render. - * - * @returns {JSX.Element} the take another bite button - */ - const takeAnotherBiteButton = useCallback(() => { - return ( - <> - {/* Icon to move to resting position */} - <Button - variant='warning' - className='mx-2 mb-2 btn-huge' - size='lg' - onClick={moveToRestingPosition} - style={{ width: buttonWidth, height: buttonHeight }} - > - <img - src={moveToRestingPositionImage} - alt='move_to_resting_image' - className='center' - style={{ width: iconWidth, height: iconHeight }} - /> - </Button> - </> - ) - }, [moveToRestingPosition, moveToRestingPositionImage, buttonHeight, buttonWidth, iconHeight, iconWidth]) + const moveToStagingConfiguration = useCallback(() => { + setMealState(MEAL_STATE.R_MovingFromMouth, MEAL_STATE.R_DetectingFace) + }, [setMealState]) /** Get the full page view * @@ -134,17 +62,85 @@ const BiteDone = () => { const fullPageView = useCallback(() => { return ( <View style={{ flex: 'auto', flexDirection: dimension, alignItems: 'center', justifyContent: 'center', width: '100%' }}> - <View style={{ flex: 5, alignItems: 'center', justifyContent: 'center' }}> - {biteFinishedText()} - {biteFinishedButton()} + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> + {/* Ask the user whether they want to move to above plate position */} + <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize }}> + Move above plate + </p> + {/* Icon to move above plate */} + <Button + variant='success' + className='mx-2 mb-2 btn-huge' + size='lg' + onClick={moveAbovePlate} + style={{ width: buttonWidth, height: buttonHeight }} + > + <img + src={moveAbovePlateImage} + alt='move_above_plate_image' + className='center' + style={{ width: iconWidth, height: iconHeight }} + /> + </Button> + </View> + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> + {/* Ask the user whether they want to move to resting position */} + <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize }}> + Rest to the side + </p> + {/* Icon to move to resting position */} + <Button + variant='warning' + className='mx-2 mb-2 btn-huge' + size='lg' + onClick={moveToRestingPosition} + style={{ width: buttonWidth, height: buttonHeight }} + > + <img + src={moveToRestingPositionImage} + alt='move_to_resting_image' + className='center' + style={{ width: iconWidth, height: iconHeight }} + /> + </Button> </View> - <View style={{ flex: 5, alignItems: 'center', justifyContent: 'center' }}> - {takeAnotherBiteText()} - {takeAnotherBiteButton()} + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}> + {/* Ask the user whether they want to move to resting position */} + <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize }}> + Move away from mouth + </p> + {/* Icon to move to resting position */} + <Button + variant='warning' + className='mx-2 mb-2 btn-huge' + size='lg' + onClick={moveToStagingConfiguration} + style={{ width: buttonWidth, height: buttonHeight }} + > + <img + src={moveToStagingConfigurationImage} + alt='move_to_staging_image' + className='center' + style={{ width: iconWidth, height: iconHeight }} + /> + </Button> </View> </View> ) - }, [dimension, biteFinishedButton, biteFinishedText, takeAnotherBiteButton, takeAnotherBiteText]) + }, [ + buttonHeight, + buttonWidth, + dimension, + iconHeight, + iconWidth, + moveAbovePlate, + moveAbovePlateImage, + moveToRestingPosition, + moveToRestingPositionImage, + moveToStagingConfiguration, + moveToStagingConfigurationImage, + textFontSize + ]) // Render the component return <>{fullPageView()}</> diff --git a/feedingwebapp/src/Pages/Home/MealStates/CircleProgressBar.jsx b/feedingwebapp/src/Pages/Home/MealStates/CircleProgressBar.jsx index dd63b880..3644ee1c 100644 --- a/feedingwebapp/src/Pages/Home/MealStates/CircleProgressBar.jsx +++ b/feedingwebapp/src/Pages/Home/MealStates/CircleProgressBar.jsx @@ -21,7 +21,7 @@ export default function CircleProgressBar(props) { // Size variables for progressbar (width, height, fontsize) in portrait and landscape let circleWidth = isPortrait ? '90%' : null let circleHeight = isPortrait ? null : '90%' - let textFontSize = isPortrait ? '10vh' : '20vh' + let textFontSize = isPortrait ? '8vh' : '14vh' // useEffect React Hook is used to synchronize with RobotMotion.jsx data to render circle progress bar useEffect(() => { diff --git a/feedingwebapp/src/Pages/Home/MealStates/DetectingFace.jsx b/feedingwebapp/src/Pages/Home/MealStates/DetectingFace.jsx index 6093ea08..77087f6b 100644 --- a/feedingwebapp/src/Pages/Home/MealStates/DetectingFace.jsx +++ b/feedingwebapp/src/Pages/Home/MealStates/DetectingFace.jsx @@ -1,5 +1,5 @@ // React Imports -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import React, { useCallback, useState } from 'react' // PropTypes is used to validate that the used props are in fact passed to this // Component import PropTypes from 'prop-types' @@ -8,18 +8,10 @@ import { useMediaQuery } from 'react-responsive' import { View } from 'react-native' // Local Imports -import { useROS, createROSService, createROSServiceRequest, subscribeToROSTopic, unsubscribeFromROSTopic } from '../../../ros/ros_helpers' import '../Home.css' -import { convertRemToPixels } from '../../../helpers' import { useGlobalState, MEAL_STATE } from '../../GlobalState' -import { - FACE_DETECTION_IMG_TOPIC, - FACE_DETECTION_TOPIC, - FACE_DETECTION_TOPIC_MSG, - MOVING_STATE_ICON_DICT, - ROS_SERVICE_NAMES -} from '../../Constants' -import VideoFeed from '../VideoFeed' +import { MOVING_STATE_ICON_DICT } from '../../Constants' +import DetectingFaceSubcomponent from './DetectingFaceSubcomponent' /** * The DetectingFace component appears after the robot has moved to the staging @@ -50,20 +42,6 @@ const DetectingFace = (props) => { let iconWidth = 28 let iconHeight = 16 let sizeSuffix = isPortrait ? 'vh' : 'vw' - // The min and max distance from the camera to the face for the face to be - // conidered valid. NOTE: This must match the values in the MoveToMouth tree. - const min_face_distance = 0.4 - const max_face_distance = 1.25 - // Margin for the video feed and between the mask buttons. Note this cannot - // be re-defined per render, otherwise it messes up re-rendering order upon - // resize in VideoFeed. - const margin = useMemo(() => convertRemToPixels(1), []) - - /** - * Connect to ROS, if not already connected. Put this in useRef to avoid - * re-connecting upon re-renders. - */ - const ros = useRef(useROS().ros) /** * Callback function for proceeding to move to the mouth position. @@ -96,75 +74,22 @@ const DetectingFace = (props) => { }, [setMealState, setMouthDetected]) /** - * Subscribe to the ROS Topic with the face detection result. This is created - * in local state to avoid re-creating it upon every re-render. + * Callback for when a face is detected within the correct range. */ - const faceDetectionCallback = useCallback( + const faceDetectedCallback = useCallback( (message) => { - console.log('Got face detection message', message) - if (message.is_face_detected) { - let distance = - (message.detected_mouth_center.point.x ** 2.0 + - message.detected_mouth_center.point.y ** 2.0 + - message.detected_mouth_center.point.z ** 2.0) ** - 0.5 - if (distance > min_face_distance && distance < max_face_distance) { - setMouthDetected(true) - setMoveToMouthActionGoal({ - face_detection: message - }) - // Automatically move on to the next stage if a face is detected - if (faceDetectionAutoContinue) { - moveToMouthCallback() - } - } + console.log('Face detected callback') + setMouthDetected(true) + setMoveToMouthActionGoal({ + face_detection: message + }) + // Automatically move on to the next stage if a face is detected + if (faceDetectionAutoContinue) { + moveToMouthCallback() } }, [faceDetectionAutoContinue, moveToMouthCallback, setMoveToMouthActionGoal] ) - useEffect(() => { - let topic = subscribeToROSTopic(ros.current, FACE_DETECTION_TOPIC, FACE_DETECTION_TOPIC_MSG, faceDetectionCallback) - /** - * In practice, because the values passed in in the second argument of - * useEffect will not change on re-renders, this return statement will - * only be called when the component unmounts. - */ - return () => { - unsubscribeFromROSTopic(topic, faceDetectionCallback) - } - }, [faceDetectionCallback]) - - /** - * Create the ROS Service. This is created in local state to avoid re-creating - * it upon every re-render. - */ - let { serviceName, messageType } = ROS_SERVICE_NAMES[MEAL_STATE.R_DetectingFace] - let toggleFaceDetectionService = useRef(createROSService(ros.current, serviceName, messageType)) - - /** - * Toggles face detection on the first time this component is rendered, but - * not upon additional re-renders. See here for more details on how `useEffect` - * achieves this goal: https://stackoverflow.com/a/69264685 - */ - useEffect(() => { - // Create a service request - let request = createROSServiceRequest({ data: true }) - // Call the service - let service = toggleFaceDetectionService.current - service.callService(request, (response) => console.log('Got toggle face detection service response', response)) - - /** - * In practice, because the values passed in in the second argument of - * useEffect will not change on re-renders, this return statement will - * only be called when the component unmounts. - */ - return () => { - // Create a service request - let request = createROSServiceRequest({ data: false }) - // Call the service - service.callService(request, (response) => console.log('Got toggle face detection service response', response)) - } - }, [toggleFaceDetectionService]) /** Get the full page view * @@ -202,36 +127,7 @@ const DetectingFace = (props) => { }} > <View style={{ flex: 5, alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> - <View - style={{ - flex: 1, - alignItems: 'center', - justifyContent: 'center', - width: '100%', - height: '100%' - }} - > - <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize.toString() + sizeSuffix }}> - {mouthDetected ? 'Mouth detected!' : 'Waiting to detect mouth...'} - </p> - </View> - <View - style={{ - flex: 9, - alignItems: 'center', - width: '100%', - height: '100%' - }} - > - <VideoFeed - marginTop={margin} - marginBottom={margin} - marginLeft={margin} - marginRight={margin} - topic={FACE_DETECTION_IMG_TOPIC} - webrtcURL={props.webrtcURL} - /> - </View> + <DetectingFaceSubcomponent faceDetectedCallback={faceDetectedCallback} webrtcURL={props.webrtcURL} /> </View> <View style={{ flex: 3, alignItems: 'center', justifyContent: 'center', width: '100%', height: '100%' }}> <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize.toString() + sizeSuffix }}> @@ -328,8 +224,8 @@ const DetectingFace = (props) => { }, [ dimension, otherDimension, - margin, mouthDetected, + faceDetectedCallback, moveToMouthCallback, moveToRestingCallback, moveAbovePlateCallback, diff --git a/feedingwebapp/src/Pages/Home/MealStates/DetectingFaceSubcomponent.jsx b/feedingwebapp/src/Pages/Home/MealStates/DetectingFaceSubcomponent.jsx new file mode 100644 index 00000000..0513c1aa --- /dev/null +++ b/feedingwebapp/src/Pages/Home/MealStates/DetectingFaceSubcomponent.jsx @@ -0,0 +1,152 @@ +// React Imports +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import PropTypes from 'prop-types' +import { useMediaQuery } from 'react-responsive' +import { View } from 'react-native' + +// Local Imports +import { useROS, createROSService, createROSServiceRequest, subscribeToROSTopic, unsubscribeFromROSTopic } from '../../../ros/ros_helpers' +import '../Home.css' +import { convertRemToPixels } from '../../../helpers' +import { MEAL_STATE } from '../../GlobalState' +import { FACE_DETECTION_IMG_TOPIC, FACE_DETECTION_TOPIC, FACE_DETECTION_TOPIC_MSG, ROS_SERVICE_NAMES } from '../../Constants' +import VideoFeed from '../VideoFeed' + +/** + * The DetectingFace component appears after the robot has moved to the staging + * configuration. It displays the output of face detection, and automatically + * moves on to `R_MovingToMouth` when a face is detected. + */ +const DetectingFaceSubcomponent = (props) => { + // Keep track of whether a mouth has been detected or not + const [mouthDetected, setMouthDetected] = useState(false) + // Flag to check if the current orientation is portrait + const isPortrait = useMediaQuery({ query: '(orientation: portrait)' }) + // Font size for text + let textFontSize = 3 + let sizeSuffix = isPortrait ? 'vh' : 'vw' + // The min and max distance from the camera to the face for the face to be + // conidered valid. NOTE: This must match the values in the MoveToMouth tree. + const min_face_distance = 0.4 + const max_face_distance = 1.25 + // Margin for the video feed and between the mask buttons. Note this cannot + // be re-defined per render, otherwise it messes up re-rendering order upon + // resize in VideoFeed. + const margin = useMemo(() => convertRemToPixels(1), []) + + /** + * Connect to ROS, if not already connected. Put this in useRef to avoid + * re-connecting upon re-renders. + */ + const ros = useRef(useROS().ros) + + /** + * Subscribe to the ROS Topic with the face detection result. This is created + * in local state to avoid re-creating it upon every re-render. + */ + const faceDetectionCallback = useCallback( + (message) => { + console.log('Got face detection message', message) + let faceDetectedCallback = props.faceDetectedCallback + if (message.is_face_detected) { + let distance = + (message.detected_mouth_center.point.x ** 2.0 + + message.detected_mouth_center.point.y ** 2.0 + + message.detected_mouth_center.point.z ** 2.0) ** + 0.5 + if (distance > min_face_distance && distance < max_face_distance) { + setMouthDetected(true) + faceDetectedCallback() + } + } + }, + [props.faceDetectedCallback, setMouthDetected] + ) + useEffect(() => { + let topic = subscribeToROSTopic(ros.current, FACE_DETECTION_TOPIC, FACE_DETECTION_TOPIC_MSG, faceDetectionCallback) + /** + * In practice, because the values passed in in the second argument of + * useEffect will not change on re-renders, this return statement will + * only be called when the component unmounts. + */ + return () => { + unsubscribeFromROSTopic(topic, faceDetectionCallback) + } + }, [faceDetectionCallback]) + + /** + * Create the ROS Service. This is created in local state to avoid re-creating + * it upon every re-render. + */ + let { serviceName, messageType } = ROS_SERVICE_NAMES[MEAL_STATE.R_DetectingFace] + let toggleFaceDetectionService = useRef(createROSService(ros.current, serviceName, messageType)) + + /** + * Toggles face detection on the first time this component is rendered, but + * not upon additional re-renders. See here for more details on how `useEffect` + * achieves this goal: https://stackoverflow.com/a/69264685 + */ + useEffect(() => { + // Create a service request + let request = createROSServiceRequest({ data: true }) + // Call the service + let service = toggleFaceDetectionService.current + service.callService(request, (response) => console.log('Got toggle face detection service response', response)) + + /** + * In practice, because the values passed in in the second argument of + * useEffect will not change on re-renders, this return statement will + * only be called when the component unmounts. + */ + return () => { + // Create a service request + let request = createROSServiceRequest({ data: false }) + // Call the service + service.callService(request, (response) => console.log('Got toggle face detection service response', response)) + } + }, [toggleFaceDetectionService]) + + // Render the component + return ( + <> + <View + style={{ + flex: 1, + alignItems: 'center', + justifyContent: 'center', + width: '100%', + height: '100%' + }} + > + <p className='transitionMessage' style={{ marginBottom: '0px', fontSize: textFontSize.toString() + sizeSuffix }}> + {mouthDetected ? 'Mouth detected!' : 'Waiting to detect mouth...'} + </p> + </View> + <View + style={{ + flex: 9, + alignItems: 'center', + width: '100%', + height: '100%' + }} + > + <VideoFeed + marginTop={margin} + marginBottom={margin} + marginLeft={margin} + marginRight={margin} + topic={FACE_DETECTION_IMG_TOPIC} + webrtcURL={props.webrtcURL} + /> + </View> + </> + ) +} +DetectingFaceSubcomponent.propTypes = { + // The URL of the webrtc signalling server + webrtcURL: PropTypes.string.isRequired, + // The function to call, with the faceDetection message as an argument, when + // a face is detected within the correct distance range. + faceDetectedCallback: PropTypes.func.isRequired +} +export default DetectingFaceSubcomponent diff --git a/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx b/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx index 7c7f66d5..07dd3809 100644 --- a/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx +++ b/feedingwebapp/src/Pages/Home/MealStates/RobotMotion.jsx @@ -18,7 +18,7 @@ import { import Footer from '../../Footer/Footer' import CircleProgressBar from './CircleProgressBar' import '../Home.css' -import { useGlobalState, MEAL_STATE } from '../../GlobalState' +import { useGlobalState } from '../../GlobalState' import { CLEAR_OCTOMAP_SERVICE_NAME, CLEAR_OCTOMAP_SERVICE_TYPE, @@ -60,8 +60,6 @@ const RobotMotion = (props) => { }) // Get the relevant global variables - const mealState = useGlobalState((state) => state.mealState) - const setMealState = useGlobalState((state) => state.setMealState) const paused = useGlobalState((state) => state.paused) const setPaused = useGlobalState((state) => state.setPaused) @@ -77,9 +75,9 @@ const RobotMotion = (props) => { // Indicator of how to arrange screen elements based on orientation let dimension = isPortrait ? 'column' : 'row' // Waiting text font size - let waitingTextFontSize = isPortrait ? '4.5vh' : '9vh' + let waitingTextFontSize = isPortrait ? '4.5vh' : '6vh' // Motion text font size - let motionTextFontSize = isPortrait ? '3vh' : '6vh' + let motionTextFontSize = isPortrait ? '3vh' : '4vh' /** * Create the ROS Action Client. This is re-created every time props.mealState @@ -89,6 +87,7 @@ const RobotMotion = (props) => { * even if it momentarily differs from the global mealState. */ let robotMotionAction = useMemo(() => { + console.log('Creating action client', props.mealState) let { actionName, messageType } = ROS_ACTIONS_NAMES[props.mealState] return createROSActionClient(ros.current, actionName, messageType) }, [props.mealState]) @@ -107,6 +106,7 @@ const RobotMotion = (props) => { */ const feedbackCallback = useCallback( (feedbackMsg) => { + console.log('Got feedback message', feedbackMsg) setActionStatus({ actionStatus: ROS_ACTION_STATUS_EXECUTE, feedback: feedbackMsg.values.feedback @@ -121,8 +121,9 @@ const RobotMotion = (props) => { */ const robotMotionDone = useCallback(() => { console.log('robotMotionDone') + let setMealState = props.setMealState setMealState(props.nextMealState) - }, [setMealState, props.nextMealState]) + }, [props.nextMealState, props.setMealState]) /** * Callback function for when the action sends a response. It updates the @@ -137,6 +138,7 @@ const RobotMotion = (props) => { */ const responseCallback = useCallback( (response) => { + console.log('Got response message', response) if (response.response_type === 'result' && response.values.status === MOTION_STATUS_SUCCESS) { setActionStatus({ actionStatus: ROS_ACTION_STATUS_SUCCEED @@ -178,6 +180,11 @@ const RobotMotion = (props) => { * is called it re-registers callbacks, so typically callbacks should only * be passed the first time it is called. * + * WARNING: If either pros.actionInput or pros.setMealState changes upon + * re-render, this function and the below useEffect will have unexpected + * behaviors (e.g., calling an action, then immediately destroying the + * action client, then calling it again, etc.) + * * @param {function} feedbackCb - the callback function for when the action * sends feedback * @param {function} responseCb - the callback function for when the action @@ -246,38 +253,11 @@ const RobotMotion = (props) => { resumeCallback() }, [clearOctomapService, resumeCallback]) - /** - * Callback function for when the back button is clicked. Regardless of the - * state, all pressed of "back" will revert to the "Moving Above Plate" state. - * - BiteAcquisition: In this case, pressing "back" should let the user - * reselect the bite, which requires the robot to move above plate. - * - MoveToRestingPostion: In this case, pressing "back" should move the - * robot back to the plate. Although the user may not always want to - * reselect the bite, from `BiteSelection` they have the option to skip - * BiteAcquisition and move straight to resting positon (when they are ready). - * - MoveToMouth: In this case, pressing "back" should move the - * robot back to the resting positon. - * - StowingArm: In this case, if the user presses back they likely want to - * eat another bite, hence moving above the plate makes sense. - * - MovingAbovePlate: Although the user may want to press "back" to move - * the robot to the mouth, they can also go forward to - * BiteSelection and then move the robot to the mouth location. - * Hence, in this case we don't have a "back" button. - */ - const backMealState = useRef(MEAL_STATE.R_MovingAbovePlate) - useEffect(() => { - if (mealState === MEAL_STATE.R_MovingToStagingConfiguration) { - backMealState.current = MEAL_STATE.R_MovingToRestingPosition - } else if (mealState === MEAL_STATE.R_MovingToMouth) { - backMealState.current = MEAL_STATE.R_MovingFromMouthToStagingConfiguration - } else { - backMealState.current = MEAL_STATE.R_MovingAbovePlate - } - }, [mealState, backMealState]) const backCallback = useCallback(() => { setPaused(false) - setMealState(backMealState.current) - }, [setPaused, setMealState, backMealState]) + let setMealState = props.setMealState + setMealState(props.backMealState) + }, [setPaused, props.backMealState, props.setMealState]) /** * Get the action status text and progress bar or blank view to render. @@ -303,7 +283,7 @@ const RobotMotion = (props) => { {props.waitingText} </p> <p style={{ fontSize: motionTextFontSize }}>{text}</p> - {showTime ? <p style={{ fontSize: motionTextFontSize }}> Elapsed Time: {time} sec</p> : <></>} + {showTime ? <p style={{ fontSize: motionTextFontSize }}> Elapsed: {time} sec</p> : <></>} {retry ? ( <Button variant='warning' @@ -359,7 +339,7 @@ const RobotMotion = (props) => { if (!actionStatus.feedback.is_planning) { let moving_elapsed_time = actionStatus.feedback.motion_time.sec + actionStatus.feedback.motion_time.nanosec / 10 ** 9 text = 'Robot is moving...' - time = Math.round(moving_elapsed_time * 100) / 100 + time = Math.round(moving_elapsed_time * 10) / 10 showTime = true progress = 1 - actionStatus.feedback.motion_curr_distance / actionStatus.feedback.motion_initial_distance // Calling CircleProgessBar component to visualize robot motion of moving @@ -367,7 +347,7 @@ const RobotMotion = (props) => { } else { let planning_elapsed_time = actionStatus.feedback.planning_time.sec + actionStatus.feedback.planning_time.nanosec / 10 ** 9 text = 'Robot is thinking...' - time = Math.round(planning_elapsed_time * 100) / 100 + time = Math.round(planning_elapsed_time * 10) / 10 showTime = true return <>{actionStatusTextAndVisual(flexSizeOuter, flexSizeTextInner, flexSizeVisualInner, text, showTime, time, progress)}</> } @@ -387,7 +367,7 @@ const RobotMotion = (props) => { * users on how to troubleshoot/fix it. */ text = 'Robot encountered an error' - retry = NON_RETRYABLE_STATES.has(mealState) ? false : true + retry = NON_RETRYABLE_STATES.has(props.mealState) ? false : true return ( <>{actionStatusTextAndVisual(flexSizeOuter, flexSizeTextInner, flexSizeVisualInner, text, showTime, time, progress, retry)}</> ) @@ -407,7 +387,7 @@ const RobotMotion = (props) => { } } }, - [paused, dimension, actionStatusTextAndVisual, mealState] + [paused, dimension, actionStatusTextAndVisual, props.mealState] ) // Render the component @@ -427,10 +407,11 @@ const RobotMotion = (props) => { * Display the footer with the Pause button. */} <Footer + mealState={props.mealState} pauseCallback={pauseCallback} - backCallback={mealState === MEAL_STATE.R_MovingAbovePlate ? null : backCallback} - backMealState={backMealState.current} - resumeCallback={NON_RETRYABLE_STATES.has(mealState) ? null : resumeCallback} + backCallback={props.backMealState ? backCallback : null} + backMealState={props.backMealState} + resumeCallback={NON_RETRYABLE_STATES.has(props.mealState) ? null : resumeCallback} paused={paused} /> </> @@ -445,12 +426,26 @@ RobotMotion.propTypes = { debug: PropTypes.bool.isRequired, // The meal state corresponding with the motion the robot is executing mealState: PropTypes.string.isRequired, + // The function for setting the meal state + // **WARNING**: If setMealState changes upon re-render, RobotMotion will have + // unexpected behaviors (e.g., calling an action, then immediately destroying + // the action client, then calling it again, etc.) + setMealState: PropTypes.func.isRequired, // The meal state to transition to once the robot finishes executing - nextMealState: PropTypes.string.isRequired, + nextMealState: PropTypes.string, + // The meal state to transition to if the user presses "back" + backMealState: PropTypes.string, // The input to provide to the ROS action + // **WARNING**: If actionInput changes upon re-render, RobotMotion will have + // unexpected behaviors (e.g., calling an action, then immediately destroying + // the action client, then calling it again, etc.) actionInput: PropTypes.object.isRequired, // The static text to display while the robot is executing the action waitingText: PropTypes.string.isRequired } +RobotMotion.defaultProps = { + debug: false +} + export default RobotMotion diff --git a/feedingwebapp/src/Pages/Home/VideoFeed.jsx b/feedingwebapp/src/Pages/Home/VideoFeed.jsx index 333cf61b..63426883 100644 --- a/feedingwebapp/src/Pages/Home/VideoFeed.jsx +++ b/feedingwebapp/src/Pages/Home/VideoFeed.jsx @@ -173,13 +173,14 @@ const VideoFeed = (props) => { let x_raw = Math.round(x / scaleFactor) // x position within the raw image. let y_raw = Math.round(y / scaleFactor) // y position within the raw image. console.log('Button click on unscaled image: (' + x_raw + ', ' + y_raw + ')') + let pointClicked = props.pointClicked // Call the callback function if it exists - if (props.pointClicked) { - props.pointClicked(x_raw, y_raw) + if (pointClicked) { + pointClicked(x_raw, y_raw) } }, - [props, scaleFactor] + [props.pointClicked, scaleFactor] ) // Render the component diff --git a/feedingwebapp/src/Pages/Settings/BiteTransfer.jsx b/feedingwebapp/src/Pages/Settings/BiteTransfer.jsx new file mode 100644 index 00000000..a4169429 --- /dev/null +++ b/feedingwebapp/src/Pages/Settings/BiteTransfer.jsx @@ -0,0 +1,510 @@ +// React imports +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import PropTypes from 'prop-types' +import { useId, Label, SpinButton } from '@fluentui/react-components' +import Button from 'react-bootstrap/Button' +// The Modal is a screen that appears on top of the main app, and can be toggled +// on and off. +import Modal from 'react-bootstrap/Modal' +import { View } from 'react-native' + +// Local imports +import { useROS, createROSService, createROSServiceRequest, getParameterValue } from '../../ros/ros_helpers' +import { + GET_PARAMETERS_SERVICE_NAME, + GET_PARAMETERS_SERVICE_TYPE, + SET_PARAMETERS_SERVICE_NAME, + SET_PARAMETERS_SERVICE_TYPE +} from '../Constants' +import { useGlobalState, MEAL_STATE, SETTINGS_STATE } from '../GlobalState' +import RobotMotion from '../Home/MealStates/RobotMotion' +import DetectingFaceSubcomponent from '../Home/MealStates/DetectingFaceSubcomponent' + +/** + * The BiteTransfer component allows users to configure parameters related to the + * bite transfer. + */ +const BiteTransfer = (props) => { + // Get relevant global state variables + const setSettingsState = useGlobalState((state) => state.setSettingsState) + const globalMealState = useGlobalState((state) => state.mealState) + const setPaused = useGlobalState((state) => state.setPaused) + const biteTransferPageAtFace = useGlobalState((state) => state.biteTransferPageAtFace) + const setBiteTransferPageAtFace = useGlobalState((state) => state.setBiteTransferPageAtFace) + + // Create relevant local state variables + // Store the current distance to mouth + const [currentDistanceToMouth, setCurrentDistanceToMouth] = useState(null) + const [localCurrAndNextMealState, setLocalCurrAndNextMealState] = useState( + globalMealState === MEAL_STATE.U_BiteDone || globalMealState === MEAL_STATE.R_DetectingFace || biteTransferPageAtFace + ? [MEAL_STATE.R_MovingFromMouth, null] + : [MEAL_STATE.R_MovingToStagingConfiguration, null] + ) + const actionInput = useMemo(() => ({}), []) + const [doneButtonIsClicked, setDoneButtonIsClicked] = useState(false) + + // Get min and max distance to mouth + const minDistanceToMouth = 1 // cm + const maxDistanceToMouth = 10 // cm + + // When we set local meal state, also update bite transfer page at face + const setLocalCurrMealStateWrapper = useCallback( + (newLocalCurrMealState, newLocalNextMealState = null) => { + let oldLocalCurrMealState = localCurrAndNextMealState[0] + // If the oldlocalCurrMealState was R_MovingToMouth, then the robot is at the mouth + setBiteTransferPageAtFace(oldLocalCurrMealState === MEAL_STATE.R_MovingToMouth) + // Start in a moving state, not a paused state + setPaused(false) + if (newLocalCurrMealState === null && doneButtonIsClicked) { + // After the done button is clicked, the robot may have to do up to two + // motions to restore itself to its old state. After that, this goes + // back to the main settings page. + setSettingsState(SETTINGS_STATE.MAIN) + } else { + setLocalCurrAndNextMealState([newLocalCurrMealState, newLocalNextMealState]) + } + }, + [localCurrAndNextMealState, setLocalCurrAndNextMealState, setBiteTransferPageAtFace, doneButtonIsClicked, setPaused, setSettingsState] + ) + + // Store the props for the RobotMotion call. The first call has the robot move + // to the staging configuration. + const robotMotionProps = useMemo(() => { + let localCurrMealState = localCurrAndNextMealState[0] + let localNextMealState = localCurrAndNextMealState[1] + let waitingText + switch (localCurrMealState) { + case MEAL_STATE.R_MovingToStagingConfiguration: + waitingText = 'Waiting to move in front of you...' + break + case MEAL_STATE.R_MovingFromMouth: + waitingText = 'Waiting to move away from you...' + break + case MEAL_STATE.R_MovingToMouth: + waitingText = 'Waiting to move to your mouth...' + break + case MEAL_STATE.R_MovingAbovePlate: + waitingText = 'Waiting to move above the plate...' + break + case MEAL_STATE.R_MovingToRestingPosition: + waitingText = 'Waiting to move to the resting position...' + break + case MEAL_STATE.R_StowingArm: + waitingText = 'Waiting to stow the arm...' + break + default: + waitingText = 'Waiting to move in front of you...' + break + } + console.log('useMemo called with', localCurrMealState, waitingText) + return { + mealState: localCurrMealState, + setMealState: setLocalCurrMealStateWrapper, + nextMealState: localNextMealState, + backMealState: null, + actionInput: actionInput, + waitingText: waitingText + } + }, [localCurrAndNextMealState, setLocalCurrMealStateWrapper, actionInput]) + + // Rendering variables + let textFontSize = '3.5vh' + + /** + * Connect to ROS, if not already connected. Put this in useRef to avoid + * re-connecting upon re-renders. + */ + const ros = useRef(useROS().ros) + + /** + * Create the ROS Service Clients to get/set parameters. + */ + let getParametersService = useRef(createROSService(ros.current, GET_PARAMETERS_SERVICE_NAME, GET_PARAMETERS_SERVICE_TYPE)) + let setParametersService = useRef(createROSService(ros.current, SET_PARAMETERS_SERVICE_NAME, SET_PARAMETERS_SERVICE_TYPE)) + + // The first time the page is rendered, get the current distance to mouth + useEffect(() => { + setDoneButtonIsClicked(false) + // Start in a moving state, not a paused state + setPaused(false) + let service = getParametersService.current + // First, attempt to get the current distance to mouth + let currentRequest = createROSServiceRequest({ + names: ['current.MoveToMouth.tree_kwargs.plan_distance_from_mouth'] + }) + service.callService(currentRequest, (response) => { + console.log('Got current plan_distance_from_mouth response', response) + if (response.values[0].type === 0) { + // Parameter not set + // Second, attempt to get the default distance to mouth + let defaultRequest = createROSServiceRequest({ + names: ['default.MoveToMouth.tree_kwargs.plan_distance_from_mouth'] + }) + service.callService(defaultRequest, (response) => { + console.log('Got default plan_distance_from_mouth response', response) + if (response.values.length > 0) { + setCurrentDistanceToMouth(getParameterValue(response.values[0])) + } + }) + } else { + setCurrentDistanceToMouth(getParameterValue(response.values[0])) + } + }) + }, [getParametersService, setCurrentDistanceToMouth, setDoneButtonIsClicked, setPaused]) + + // Callback to set the distance to mouth parameter + const setDistanceToMouth = useCallback( + (fullDistanceToMouth) => { + let service = setParametersService.current + let request = createROSServiceRequest({ + parameters: [ + { + name: 'current.MoveToMouth.tree_kwargs.plan_distance_from_mouth', + value: { + type: 8, // double array + double_array_value: fullDistanceToMouth + } + } + ] + }) + service.callService(request, (response) => { + console.log('Got response', response) + if (response != null && response.results.length > 0 && response.results[0].successful) { + setCurrentDistanceToMouth(fullDistanceToMouth) + } + }) + }, + [setParametersService, setCurrentDistanceToMouth] + ) + + // Callback to restore the distance to mouth to the default + const restoreToDefaultButtonClicked = useCallback(() => { + let service = getParametersService.current + // Attempt to get the default distance to mouth + let defaultRequest = createROSServiceRequest({ + names: ['default.MoveToMouth.tree_kwargs.plan_distance_from_mouth'] + }) + service.callService(defaultRequest, (response) => { + console.log('Got default plan_distance_from_mouth response', response) + if (response.values.length > 0) { + setDistanceToMouth(getParameterValue(response.values[0])) + } + }) + }, [getParametersService, setDistanceToMouth]) + + // Callback to move the robot to the mouth + const moveToMouthButtonClicked = useCallback(() => { + setLocalCurrMealStateWrapper(MEAL_STATE.R_DetectingFace) + setDoneButtonIsClicked(false) + }, [setLocalCurrMealStateWrapper, setDoneButtonIsClicked]) + + // Callback to move the robot away from the mouth + const moveAwayFromMouthButtonClicked = useCallback(() => { + setLocalCurrMealStateWrapper(MEAL_STATE.R_MovingFromMouth) + setDoneButtonIsClicked(false) + }, [setLocalCurrMealStateWrapper, setDoneButtonIsClicked]) + + // Callback to return to the main settings page + const doneButtonClicked = useCallback(() => { + setDoneButtonIsClicked(true) + // Determine the state to move to based on the state before entering settings + let localNextMealState + // To get to Settings, the globalMealState must be one of the NON_MOVING_STATES + switch (globalMealState) { + case MEAL_STATE.U_BiteDone: + localNextMealState = null + break + case MEAL_STATE.U_PreMeal: + case MEAL_STATE.U_BiteSelection: + localNextMealState = MEAL_STATE.R_MovingAbovePlate + break + case MEAL_STATE.U_BiteAcquisitionCheck: + localNextMealState = MEAL_STATE.R_MovingToRestingPosition + break + case MEAL_STATE.U_PostMeal: + localNextMealState = MEAL_STATE.R_StowingArm + break + default: + localNextMealState = MEAL_STATE.R_MovingAbovePlate + break + } + setLocalCurrMealStateWrapper(MEAL_STATE.R_MovingFromMouth, localNextMealState) + }, [globalMealState, setLocalCurrMealStateWrapper, setDoneButtonIsClicked]) + + // Callback for when the user changes the distance to mouth + const onDistanceToMouthChange = useCallback( + (_ev, data) => { + let value = data.value ? data.value : parseFloat(data.displayValue) + if (value < minDistanceToMouth) { + value = minDistanceToMouth + } + if (value > maxDistanceToMouth) { + value = maxDistanceToMouth + } + let fullDistanceToMouth = [value / 100.0, currentDistanceToMouth[1], currentDistanceToMouth[2]] + setDistanceToMouth(fullDistanceToMouth) + }, + [setDistanceToMouth, currentDistanceToMouth, minDistanceToMouth, maxDistanceToMouth] + ) + + // Callback to render the main contents of the page + const distanceToMouthId = useId() + const renderBiteTransferSettings = useCallback(() => { + if (currentDistanceToMouth === null) { + return ( + <> + <View + style={{ + flex: 1, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + > + <h5 style={{ textAlign: 'center', fontSize: textFontSize }}>Loading...</h5> + </View> + </> + ) + } else { + return ( + <> + <View + style={{ + flex: 8, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + > + <Label + htmlFor={distanceToMouthId} + style={{ + fontSize: textFontSize, + width: '90%', + color: 'black', + textAlign: 'center' + }} + > + Distance To Mouth (cm) + </Label> + <SpinButton + value={currentDistanceToMouth[0] * 100} + id={distanceToMouthId} + step={0.5} + onChange={onDistanceToMouthChange} + appearance='filled-lighter' + style={{ + fontSize: textFontSize, + width: '90%', + color: 'black' + }} + incrementButton={{ + 'aria-label': 'Increase value by 0.5', + 'aria-roledescription': 'spinner', + size: 'large' + }} + /> + <Button + variant='warning' + className='mx-2 mb-2 btn-huge' + size='lg' + style={{ + fontSize: textFontSize, + width: '60%', + color: 'black' + }} + onClick={restoreToDefaultButtonClicked} + > + Set to Default + </Button> + </View> + <View + style={{ + flex: 2, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + > + <Button + variant='warning' + className='mx-2 mb-2 btn-huge' + size='lg' + style={{ + fontSize: textFontSize, + width: '90%', + height: '90%', + color: 'black' + }} + onClick={moveToMouthButtonClicked} + > + Move To Mouth + </Button> + </View> + <View + style={{ + flex: 2, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + /> + <View + style={{ + flex: 2, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + > + <Button + variant='warning' + className='mx-2 mb-2 btn-huge' + size='lg' + style={{ + fontSize: textFontSize, + width: '90%', + height: '90%', + color: 'black' + }} + onClick={moveAwayFromMouthButtonClicked} + > + Move From Mouth + </Button> + </View> + <View + style={{ + flex: 2, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + /> + </> + ) + } + }, [ + textFontSize, + currentDistanceToMouth, + onDistanceToMouthChange, + distanceToMouthId, + moveToMouthButtonClicked, + moveAwayFromMouthButtonClicked, + restoreToDefaultButtonClicked + ]) + + // When a face is detected, switch to MoveToMouth + const faceDetectedCallback = useCallback(() => { + setLocalCurrMealStateWrapper(MEAL_STATE.R_MovingToMouth) + }, [setLocalCurrMealStateWrapper]) + + // Render the modal body, for calling robot code from within this settings page + const renderModalBody = useCallback(() => { + let localCurrMealState = localCurrAndNextMealState[0] + switch (localCurrMealState) { + case MEAL_STATE.R_MovingToStagingConfiguration: + case MEAL_STATE.R_MovingFromMouth: + case MEAL_STATE.R_MovingToMouth: + case MEAL_STATE.R_MovingAbovePlate: + case MEAL_STATE.R_MovingToRestingPosition: + case MEAL_STATE.R_StowingArm: + return ( + <RobotMotion + mealState={robotMotionProps.mealState} + setMealState={robotMotionProps.setMealState} + nextMealState={robotMotionProps.nextMealState} + backMealState={robotMotionProps.backMealState} + actionInput={robotMotionProps.actionInput} + waitingText={robotMotionProps.waitingText} + /> + ) + case MEAL_STATE.R_DetectingFace: + return <DetectingFaceSubcomponent faceDetectedCallback={faceDetectedCallback} webrtcURL={props.webrtcURL} /> + default: + return <></> + } + }, [localCurrAndNextMealState, props.webrtcURL, robotMotionProps, faceDetectedCallback]) + + return ( + <> + <View + style={{ + flex: 2, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + > + <h5 style={{ textAlign: 'center', fontSize: textFontSize }}>Customize Bite Transfer</h5> + </View> + <View + style={{ + flex: 16, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + > + {renderBiteTransferSettings()} + </View> + <View + style={{ + flex: 2, + flexDirection: 'column', + justifyContent: 'center', + alignItems: 'center', + width: '100%' + }} + > + <Button + variant='success' + className='mx-2 mb-2 btn-huge' + size='lg' + style={{ + fontSize: textFontSize, + width: '90%', + height: '90%', + color: 'black' + }} + onClick={doneButtonClicked} + > + Done + </Button> + </View> + <Modal + show={localCurrAndNextMealState[0] !== null} + onHide={() => setLocalCurrMealStateWrapper(null)} + size='lg' + aria-labelledby='contained-modal-title-vcenter' + backdrop='static' + keyboard={false} + centered + id='robotMotionModal' + fullscreen={false} + dialogClassName='modal-90w' + style={{ + '--bs-modal-padding': '0rem' + }} + > + <Modal.Header closeButton /> + <Modal.Body style={{ overflow: 'hidden' }}> + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', height: '65vh' }}>{renderModalBody()}</View> + </Modal.Body> + </Modal> + </> + ) +} +BiteTransfer.propTypes = { + // The URL of the webrtc signalling server + webrtcURL: PropTypes.string.isRequired +} + +export default BiteTransfer diff --git a/feedingwebapp/src/Pages/Settings/Main.jsx b/feedingwebapp/src/Pages/Settings/Main.jsx new file mode 100644 index 00000000..1bf0a1af --- /dev/null +++ b/feedingwebapp/src/Pages/Settings/Main.jsx @@ -0,0 +1,112 @@ +// React imports +import React from 'react' +import Button from 'react-bootstrap/Button' +import Container from 'react-bootstrap/Container' +import Row from 'react-bootstrap/Row' +import Image from 'react-bootstrap/Image' + +// Local imports +import { MOVING_STATE_ICON_DICT } from '../Constants' +import { useGlobalState, /* SETTINGS, */ MEAL_STATE, SETTINGS_STATE } from '../GlobalState' +// import ToggleButtonGroup from '../../buttons/ToggleButtonGroup' + +/** + * The Main component displays all the settings users are able to configure. + */ +const Main = () => { + // Get relevant global state variables + const setSettingsState = useGlobalState((state) => state.setSettingsState) + + // Get icon image for move to mouth + let moveToMouthConfigurationImage = MOVING_STATE_ICON_DICT[MEAL_STATE.R_MovingToMouth] + + return ( + <Container fluid> + {/** + * The title of the page. + */} + <Row className='justify-content-center mx-1 my-2'> + <h1 style={{ textAlign: 'center', fontSize: '40px' }} className='txt-huge'> + ⚙ Settings + </h1> + </Row> + + <Row className='justify-content-center mx-1 my-2'> + <Button + variant='outline-dark' + style={{ + fontSize: '30px', + display: 'flex', + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + height: '8vh', + borderWidth: '3px' + }} + onClick={() => setSettingsState(SETTINGS_STATE.BITE_TRANSFER)} + > + <Image + fluid + src={moveToMouthConfigurationImage} + style={{ + height: '100%', + '--bs-btn-padding-x': '0rem', + '--bs-btn-padding-y': '0rem', + display: 'flex' + }} + alt='move_to_mouth_image' + className='center' + /> + <p + style={{ + display: 'flex', + marginTop: '1rem' + }} + > + Bite Transfer + </p> + </Button> + </Row> + + {/** + * Load toggle-able buttons for all the settings. + * + * TODO: + * - Instead of having a full sentence "title" for every setting, we + * should have a brief title with an optional "i" on the right side + * that users can click for additional information to pop up (perhaps + * as a Modal?) + * - We shouldn't assume all settings will be ToggleButtonGroup. + * For example, bite initiation settings should instead be checkboxes. + */} + {/* <Row className='justify-content-center mx-1 my-2'> + <Form.Label style={{ fontSize: '30px' }}>Where should the robot wait after it gets food? </Form.Label> + <ToggleButtonGroup + valueOptions={SETTINGS.stagingPosition} + currentValue={useGlobalState((state) => state.stagingPosition)} + valueSetter={useGlobalState((state) => state.setStagingPosition)} + /> + </Row> + + <Row className='justify-content-center mx-1 my-2'> + <Form.Label style={{ fontSize: '30px' }}>How do you want to indicate readiness for a bite? </Form.Label> + <ToggleButtonGroup + valueOptions={SETTINGS.biteInitiation} + currentValue={useGlobalState((state) => state.biteInitiation)} + valueSetter={useGlobalState((state) => state.setBiteInitiation)} + /> + </Row> + + <Row className='justify-content-center mx-1 my-2'> + <Form.Label style={{ fontSize: '30px' }}>How do you want to select your desired food item? </Form.Label> + <ToggleButtonGroup + valueOptions={SETTINGS.biteSelection} + currentValue={useGlobalState((state) => state.biteSelection)} + valueSetter={useGlobalState((state) => state.setBiteSelection)} + /> + </Row> */} + </Container> + ) +} + +export default Main diff --git a/feedingwebapp/src/Pages/Settings/Settings.jsx b/feedingwebapp/src/Pages/Settings/Settings.jsx index b0865e87..c9aa3169 100644 --- a/feedingwebapp/src/Pages/Settings/Settings.jsx +++ b/feedingwebapp/src/Pages/Settings/Settings.jsx @@ -1,67 +1,47 @@ // React imports -import React from 'react' -import Row from 'react-bootstrap/Row' -import Form from 'react-bootstrap/Form' +import React, { useCallback } from 'react' +import PropTypes from 'prop-types' +import { View } from 'react-native' // Local imports -import { useGlobalState, SETTINGS } from '../GlobalState' -import ToggleButtonGroup from '../../buttons/ToggleButtonGroup' +import { useGlobalState, SETTINGS_STATE } from '../GlobalState' +import Main from './Main' +import BiteTransfer from './BiteTransfer' /** - * The Settings components displays all the settings users are able to configure. - * Since settings are stored in global state, options for the settings should be - * stored in GlobalState.js and this component should primarily focus on - * rendering them. + * The Settings components displays the appropriate settings page based on the + * current settings state. */ -const Settings = () => { - return ( - <div> - {/** - * The title of the page. - */} - <h1 style={{ textAlign: 'center', fontSize: '40px' }} className='txt-huge'> - ⚙ Settings - </h1> +const Settings = (props) => { + // Get the relevant values from global state + const settingsState = useGlobalState((state) => state.settingsState) + + const getComponentBySettingsState = useCallback(() => { + console.log('getComponentBySettingsState', settingsState) + switch (settingsState) { + case SETTINGS_STATE.MAIN: + return <Main /> + case SETTINGS_STATE.BITE_TRANSFER: + return <BiteTransfer webrtcURL={props.webrtcURL} /> + default: + console.log('Invalid settings state', settingsState) + return <Main /> + } + }, [props.webrtcURL, settingsState]) + // Render the component + return ( + <View style={{ flex: 1, alignItems: 'center', justifyContent: 'start' }}> {/** - * Load toggle-able buttons for all the settings. - * - * TODO: - * - Instead of having a full sentence "title" for every setting, we - * should have a brief title with an optional "i" on the right side - * that users can click for additional information to pop up (perhaps - * as a Modal?) - * - We shouldn't assume all settings will be ToggleButtonGroup. - * For example, bite initiation settings should instead be checkboxes. + * The main contents of the screen depends on the settingsState. */} - <Row className='justify-content-center mx-1 my-2'> - <Form.Label style={{ fontSize: '30px' }}>Where should the robot wait after it gets food? </Form.Label> - <ToggleButtonGroup - valueOptions={SETTINGS.stagingPosition} - currentValue={useGlobalState((state) => state.stagingPosition)} - valueSetter={useGlobalState((state) => state.setStagingPosition)} - /> - </Row> - - <Row className='justify-content-center mx-1 my-2'> - <Form.Label style={{ fontSize: '30px' }}>How do you want to indicate readiness for a bite? </Form.Label> - <ToggleButtonGroup - valueOptions={SETTINGS.biteInitiation} - currentValue={useGlobalState((state) => state.biteInitiation)} - valueSetter={useGlobalState((state) => state.setBiteInitiation)} - /> - </Row> - - <Row className='justify-content-center mx-1 my-2'> - <Form.Label style={{ fontSize: '30px' }}>How do you want to select your desired food item? </Form.Label> - <ToggleButtonGroup - valueOptions={SETTINGS.biteSelection} - currentValue={useGlobalState((state) => state.biteSelection)} - valueSetter={useGlobalState((state) => state.setBiteSelection)} - /> - </Row> - </div> + {getComponentBySettingsState()} + </View> ) } +Settings.propTypes = { + // The URL of the webrtc signalling server + webrtcURL: PropTypes.string.isRequired +} export default Settings diff --git a/feedingwebapp/src/ros/ros_helpers.js b/feedingwebapp/src/ros/ros_helpers.js index f94b200a..65c228e8 100644 --- a/feedingwebapp/src/ros/ros_helpers.js +++ b/feedingwebapp/src/ros/ros_helpers.js @@ -170,3 +170,35 @@ export function cancelROSAction(actionClient) { export function destroyActionClient(actionClient) { actionClient.destroyClient() } + +/** + * Takes in an object of type ParameterValue and returns the actual parameter + * value. See the message definition for more details: + * https://github.com/ros2/rcl_interfaces/blob/rolling/rcl_interfaces/msg/ParameterValue.msg + * + * @param {object} parameterValue an object of message type ParameterValue + */ +export function getParameterValue(parameterValue) { + switch (parameterValue.type) { + case 1: // bool + return parameterValue.bool_value + case 2: // integer + return parameterValue.integer_value + case 3: // double + return parameterValue.double_value + case 4: // string + return parameterValue.string_value + case 5: // byte array + return parameterValue.byte_array_value + case 6: // bool array + return parameterValue.bool_array_value + case 7: // integer array + return parameterValue.integer_array_value + case 8: // double array + return parameterValue.double_array_value + case 9: // string array + return parameterValue.string_array_value + default: // not set + return null + } +} diff --git a/feedingwebapp/src/webrtc/webrtc_helpers.js b/feedingwebapp/src/webrtc/webrtc_helpers.js index 4d04959a..bafae5ab 100644 --- a/feedingwebapp/src/webrtc/webrtc_helpers.js +++ b/feedingwebapp/src/webrtc/webrtc_helpers.js @@ -4,7 +4,7 @@ import axios from 'axios' /** * Creates a connection to the WebRTC signalling server defined in `server.js`. - * + * * @param {Object} options * @param {string} options.url - The URL of the WebRTC signalling server. * @param {string} options.topic - The topic to subscribe to. @@ -13,7 +13,7 @@ import axios from 'axios' * @param {string} options.transceiverKind - The kind of transceiver to add to the peer connection. * @param {Object} options.transceiverOptions - The options for the transceiver. * @param {MediaStream} options.stream - The stream to add to the peer connection. - * + * * @returns {WebRTCConnection} The WebRTC connection. */ export class WebRTCConnection {