diff --git a/package-lock.json b/package-lock.json index 1ce30e3..dcfd227 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "krow-demo", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^4.1.2", + "@mui/material": "^9.3.1", + "@mui/x-charts": "^9.11.1", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-aspect-ratio": "^1.1.2", @@ -98,7 +102,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", @@ -154,7 +157,6 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.29.8", @@ -188,7 +190,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -198,7 +199,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.29.7", @@ -240,7 +240,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -250,7 +249,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -284,7 +282,6 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.29.8" @@ -341,7 +338,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -356,7 +352,6 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -375,7 +370,6 @@ "version": "7.29.8", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.29.7", @@ -385,6 +379,180 @@ "node": ">=6.9.0" } }, + "node_modules/@base-ui/utils": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@base-ui/utils/-/utils-0.3.2.tgz", + "integrity": "sha512-oWy1aq/I2GmYjpl4PhEAhzflF8VPGKgZeq0xAWTbfD5KBWyxcN0ZP2+WHSUm/5Z6lVMBDLReLcoXwSYoRc/zNQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.2", + "@floating-ui/utils": "^0.2.12", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "@types/react": "^17 || ^18 || ^19", + "react": "^17 || ^18 || ^19", + "react-dom": "^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -1146,6 +1314,328 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-9.3.1.tgz", + "integrity": "sha512-IAyAFNQbT7hysJ9HXphiOmWJF7G1OglzHanqCgvQgH9LA2ydxtmaTBDbcBqw6euZesyShiwvpvbnYO1GY1AyXQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/material": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-9.3.1.tgz", + "integrity": "sha512-NahAEGIXqS1K0bA4th1jeFxBguS59NOcLbMA0vU+fSaPWKjtwGBGGHeTwlc9PSmzMjOZKceeFWESB8fVHr31hA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/core-downloads-tracker": "^9.3.1", + "@mui/system": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1", + "react-is": "^19.2.8", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^9.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-9.3.0.tgz", + "integrity": "sha512-ERvqk5pejf9aRnQcDILSWGtFmsEMiVxlQ4+xsVCjsEvmK0fV9BiVP/cQwAF5dwyDFbve4lTrlcgeFEecVzTNiA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.3.0", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-9.3.0.tgz", + "integrity": "sha512-x9+KYxhjoHYZ4nioxdKnvQWdw0RScbhoZfQ4tv3Db742683U3wlYSp6zV6Us78dMFnKnyTrPTkQKyutp14gnKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-9.3.0.tgz", + "integrity": "sha512-0l4LqHJxZj65xSrioniGsxm7VNoGXonPo203oZjhBUvIDPeBqRTb7Mqc45Qxs6sO6WGR7WE/9cJ6lb4lkvHkjg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/private-theming": "^9.3.0", + "@mui/styled-engine": "^9.3.0", + "@mui/types": "^9.3.0", + "@mui/utils": "^9.3.0", + "clsx": "^2.1.1", + "csstype": "^3.2.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-9.3.0.tgz", + "integrity": "sha512-2JSxyfpEFWNUB2vKs/T1BvkfyNisMHWph8bLMj8T0uHwmLl/0qfAwQkfwMT6kxLXN9uIum9AEbECXU8er3amIg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-9.3.0.tgz", + "integrity": "sha512-2HZdHwWJ6eB+7lVGSOHsByGw8jeRulT4g0NZ608Wb8Q57DE2jbNqrWPFuJsvkQQiBiTmlpvQL3i+/62zsiPrkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/types": "^9.3.0", + "@types/prop-types": "^15.7.15", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.2.8" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/@mui/x-charts": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-9.11.1.tgz", + "integrity": "sha512-5Z6fzVm0NvD4auoyGZopp1wwg1mMVDXjbPEi4jahdxn14EzIAB99kKw4NUkKtmSaZJ6DiBU7zJXQAec1IZTaLA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@mui/utils": "^9.3.0", + "@mui/x-charts-vendor": "^9.11.1", + "@mui/x-internal-gestures": "^9.10.0", + "@mui/x-internals": "^9.11.0", + "bezier-easing": "^3.0.1", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^7.3.0 || ^9.0.0", + "@mui/system": "^7.3.0 || ^9.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "9.11.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-9.11.1.tgz", + "integrity": "sha512-Q7cuPaKZLLsyvtEiP+ENG7xLlPatXyNQ7u5WkbAcAIZwyMK8MJw4MPgJiyWyfy9eVQO/IkRLIupabvu8cA2jPA==", + "license": "MIT AND ISC", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@types/d3-array": "^3.2.2", + "@types/d3-color": "^3.1.3", + "@types/d3-format": "^3.0.4", + "@types/d3-geo": "^3.1.0", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-path": "^3.1.1", + "@types/d3-scale": "^4.0.9", + "@types/d3-shape": "^3.1.8", + "@types/d3-time": "^3.0.4", + "@types/d3-time-format": "^4.0.3", + "@types/d3-timer": "^3.0.2", + "d3-array": "^3.2.4", + "d3-color": "^3.1.0", + "d3-format": "^3.1.2", + "d3-geo": "^3.1.1", + "d3-interpolate": "^3.0.1", + "d3-path": "^3.1.0", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "d3-time-format": "^4.1.0", + "d3-timer": "^3.0.1", + "flatqueue": "^3.1.0", + "internmap": "^2.0.3" + } + }, + "node_modules/@mui/x-internal-gestures": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/@mui/x-internal-gestures/-/x-internal-gestures-9.10.0.tgz", + "integrity": "sha512-Fgmh+vJlOjLst238/J5SlEOZLdnSe0yWjrYctYVWJr3pKbtmYSbDkjYvWjG97PeDvq5Wa1+W1qagiBoJgkUIng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1" + } + }, + "node_modules/@mui/x-internals": { + "version": "9.11.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-9.11.0.tgz", + "integrity": "sha512-JjKe9k1+gVWNPwMTZLNSc92eVmoZqP5Xq3Ui6mJkstotUrbWIZC0o9+4AfTR1lYqWwnKmqm+VtLLVCa7MEBXWQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "@base-ui/utils": "^0.3.1", + "@mui/utils": "^9.3.0", + "reselect": "^5.2.0", + "use-sync-external-store": "^1.6.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@napi-rs/lzma-linux-x64-gnu": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", @@ -1198,6 +1688,16 @@ "node": ">= 8" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@radix-ui/number": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.3.tgz", @@ -2993,6 +3493,21 @@ "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", "license": "MIT" }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", @@ -3032,6 +3547,12 @@ "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", "license": "MIT" }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", @@ -3062,6 +3583,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", @@ -3103,6 +3630,12 @@ "undici-types": "~6.21.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.15", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", @@ -3129,6 +3662,15 @@ "@types/react": "^18.0.0" } }, + "node_modules/@types/react-transition-group": { + "version": "4.4.12", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz", + "integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*" + } + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -3463,6 +4005,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/babel-plugin-macros/node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/bail": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", @@ -3502,6 +4080,12 @@ "node": ">=6.0.0" } }, + "node_modules/bezier-easing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-3.0.1.tgz", + "integrity": "sha512-vvvg10Anq7py0+KkIqlwJpUVdde9Z8kSzeUzAkQNYAJdA3mZnLG9r9ExdwtjcASCYER6UkNHdBNHHMnVimnU7A==", + "license": "MIT" + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3625,7 +4209,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -3854,6 +4437,31 @@ "dev": true, "license": "MIT" }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", + "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -3935,6 +4543,18 @@ "node": ">=12" } }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/d3-interpolate": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", @@ -4273,6 +4893,15 @@ "embla-carousel": "8.6.0" } }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.24.2", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.2.tgz", @@ -4527,7 +5156,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -4876,6 +5504,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -4907,6 +5541,12 @@ "node": ">=16" } }, + "node_modules/flatqueue": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/flatqueue/-/flatqueue-3.1.0.tgz", + "integrity": "sha512-Ia4qIYrrsEqIRx3c3XhkT+QDLQuUV5ovsr6ah1rIgKT5wclhoGK3lAMS1bWRAWxlx7wtlTBpV7QXB5d9fOSRxA==", + "license": "ISC" + }, "node_modules/flatted": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", @@ -5311,6 +5951,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -5348,7 +5997,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -5453,6 +6101,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, "node_modules/is-async-function": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", @@ -5957,7 +6611,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -5973,6 +6626,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -7071,7 +7730,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -7105,6 +7763,24 @@ "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "license": "MIT" }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7131,6 +7807,15 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "license": "MIT" }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -7809,6 +8494,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/reselect": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.2.0.tgz", + "integrity": "sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==", + "license": "MIT" + }, "node_modules/resolve": { "version": "2.0.0-next.7", "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.7.tgz", @@ -7837,7 +8528,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -8154,6 +8844,15 @@ "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -8331,6 +9030,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -8882,6 +9587,15 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -9188,6 +9902,23 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/package.json b/package.json index 2519c41..62feab7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,11 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^4.1.2", + "@mui/material": "^9.3.1", + "@mui/x-charts": "^9.11.1", "@radix-ui/react-accordion": "^1.2.3", "@radix-ui/react-alert-dialog": "^1.1.6", "@radix-ui/react-aspect-ratio": "^1.1.2", diff --git a/src/components/ai-assistant/capabilities/admin.js b/src/components/ai-assistant/capabilities/admin.js index 5450639..7c4083e 100644 --- a/src/components/ai-assistant/capabilities/admin.js +++ b/src/components/ai-assistant/capabilities/admin.js @@ -1,12 +1,14 @@ import { - Activity, AlertTriangle, ArrowLeftRight, BellRing, Eye, Filter, Gauge, GitBranch, - Layers, Lightbulb, ScrollText, ShieldAlert, Target, TrendingUp, UserCheck, Users, + Activity, AlertTriangle, ArrowLeftRight, Award, BellRing, Clock, Eye, Filter, Gauge, + GitBranch, Layers, Lightbulb, Lock, ScrollText, ShieldAlert, ShieldCheck, Star, Target, + TrendingUp, UserCheck, Users, } from 'lucide-react'; import { actions, answer, doc, funnel, heading, insights, kpis, list, meters, note, status, table, text, timeline, } from '../blocks'; -import { plural, verb } from '../insights'; +import { pct, plural, verb } from '../insights'; +import { toFICO } from '@/lib/talentHome'; /** * Admin capabilities — Control Center, Candidates Analysis, Activity. @@ -1351,3 +1353,463 @@ export const ACTIVITY_CAPABILITIES = [ { id: 'unusual-activity', label: 'Unusual Activity', icon: AlertTriangle, run: unusualActivity }, { id: 'security-insights', label: 'Security Insights', icon: ShieldAlert, run: securityInsights }, ]; + +/* ── KROW Forge ─────────────────────────────────────────────────────────── */ + +/** + * Forge capabilities — workforce skill verification. + * + * The Forge questions are not hiring questions: they are about proof. What is + * verified, what can be proven next, and what is gating the rest. Every figure + * comes from `f.forge`, which is derived with the same unlock gate the page + * renders from. + */ + +const forgeProgress = (f) => { + const g = f.forge; + return doc( + heading('Forge progress', `${plural(g.courses.length, 'challenge')} in the library`), + kpis([ + { label: 'Verified skills', value: g.verified.length, sub: g.verified.length ? 'proven' : 'nothing proven yet' }, + { label: 'In progress', value: g.inProgress.length, sub: 'challenges started' }, + { label: 'Ready to start', value: g.ready.length, sub: 'unlocked now' }, + { label: 'XP available', value: g.xpAvailable, sub: `across ${plural(g.courses.length - g.completed.length, 'challenge')}` }, + ]), + g.verified.length + ? list(g.verified.map((b) => `**${b.name}** — ${b.level || 'Standard'}, verified`)) + : text('No skill is verified yet. The first one comes from completing a challenge and submitting proof for Owliver to grade.'), + g.byDifficulty.length ? heading('Library by difficulty') : null, + meters(g.byDifficulty.map((d) => ({ + label: d.level.charAt(0).toUpperCase() + d.level.slice(1), + value: d.count, + max: g.courses.length, + }))), + g.locked.length + ? note(`${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} still gated by real-work requirements.`) + : null + ); +}; + +const forgeNextChallenge = (f) => { + const g = f.forge; + if (!g.next) return answer('Every active challenge is complete — there is nothing left to start.'); + + const c = g.next; + const gate = g.blocked.find((b) => b.course.id === c.id); + + return doc( + heading(c.title, `${c.proof_skill || c.category} · ${c.difficulty}`), + text(c.description), + kpis([ + { label: 'Duration', value: `${c.estimated_minutes} min` }, + { label: 'XP', value: c.xp }, + { label: 'Earns', value: c.badge_reward || '—' }, + { label: 'State', value: gate ? 'Locked' : 'Ready', tone: gate ? 'warning' : 'success' }, + ]), + gate + ? insights([{ + tone: 'warning', + title: 'Blocked for now', + body: `${gate.reasons.join('. ')}. Those are real-work requirements, so they clear through shifts rather than through study.`, + }]) + : insights([{ + tone: 'success', + title: 'Ready to start', + body: `Nothing is gating this one. ${plural(c.estimated_minutes, 'minute')} of work is worth ${c.xp} XP${c.badge_reward ? ` and the ${c.badge_reward} badge` : ''}.`, + }]), + g.ready.length > 1 + ? actions(g.ready.slice(1, 3).map((alt) => ({ + title: alt.title, + body: `${alt.difficulty} · ${alt.estimated_minutes} min · ${alt.xp} XP. An alternative if this one does not fit today.`, + }))) + : null + ); +}; + +const forgeBlockers = (f) => { + const g = f.forge; + if (!g.locked.length) { + return answer(`Nothing is locked — all ${plural(g.ready.length, 'remaining challenge')} can be started now.`); + } + + return doc( + heading(`${plural(g.locked.length, 'challenge')} blocked`, 'and what clears each one'), + table( + [ + { key: 'challenge', label: 'Challenge' }, + { key: 'skill', label: 'Skill' }, + { key: 'requirement', label: 'Blocked by' }, + ], + g.blocked.map(({ course, reasons }) => ({ + challenge: course.title, + skill: course.proof_skill || course.category, + requirement: reasons[0] || '—', + })) + ), + insights(g.blocked.slice(0, 2).map(({ course, reasons }) => ({ + tone: 'warning', + title: course.title, + body: `${reasons.join('. ')}. Worth ${course.xp} XP${course.badge_reward ? ` and the ${course.badge_reward} badge` : ''} once it opens.`, + }))), + note('Requirements are measured against the profile record, so they update as shifts and reliability are logged.') + ); +}; + +const forgeReadyNow = (f) => { + const g = f.forge; + if (!g.ready.length) { + return doc( + text('Nothing is unlocked right now.'), + g.locked.length ? text(`${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} waiting on real-work requirements.`) : null + ); + } + + return doc( + heading(`${plural(g.ready.length, 'challenge')} ready to start`), + table( + [ + { key: 'challenge', label: 'Challenge' }, + { key: 'difficulty', label: 'Difficulty' }, + { key: 'minutes', label: 'Min', align: 'right' }, + { key: 'xp', label: 'XP', align: 'right' }, + ], + g.ready.map((c) => ({ + challenge: c.title, + difficulty: c.difficulty, + minutes: c.estimated_minutes, + xp: c.xp, + })) + ), + text( + `Cheapest first: **${[...g.ready].sort((a, b) => a.estimated_minutes - b.estimated_minutes)[0].title}**. ` + + `Highest value: **${[...g.ready].sort((a, b) => b.xp - a.xp)[0].title}**.` + ) + ); +}; + +const forgeSkillMap = (f) => { + const g = f.forge; + const skills = g.courses.map((c) => ({ + skill: c.proof_skill || c.category || 'Workforce skill', + state: g.verified.some((b) => b.course_id === c.id) + ? 'Verified' + : g.locked.some((l) => l.id === c.id) ? 'Locked' : 'Ready', + xp: c.xp, + })); + + return doc( + heading('Skills this library can verify', plural(skills.length, 'skill')), + table( + [ + { key: 'skill', label: 'Skill' }, + { key: 'state', label: 'State' }, + { key: 'xp', label: 'XP', align: 'right' }, + ], + skills + ), + text( + g.verified.length + ? `${plural(g.verified.length, 'skill')} already carries evidence an employer can read.` + : 'None of these carry evidence yet. Each verified skill adds a record an employer can read against the profile.' + ) + ); +}; + +/* ── Talent pool ────────────────────────────────────────────────────────── */ + +const talentPoolSummary = (f) => { + const t = f.talent; + return doc( + heading('Talent pool', `${plural(f.profiles.length, 'registered worker')}`), + kpis([ + { label: 'Profiles', value: f.profiles.length, sub: `${t.available.length} available` }, + { label: 'Elite', value: f.elite.length, sub: '90+ career score' }, + { label: 'Average score', value: t.avgScore ? toFICO(t.avgScore) : '—', sub: 'scored profiles only' }, + { label: 'Unscored', value: f.unscoredProfiles.length, sub: 'cannot be ranked', tone: f.unscoredProfiles.length ? 'warning' : undefined }, + ]), + heading('Score bands'), + meters(t.bands.map((b) => ({ label: b.label, value: b.count, max: f.profiles.length }))), + text( + f.unscoredProfiles.length + ? `${plural(f.unscoredProfiles.length, 'profile')} ${verb(f.unscoredProfiles.length, 'has', 'have')} no career score, so ${verb(f.unscoredProfiles.length, 'it is', 'they are')} invisible to ranking and matching.` + : 'Every profile carries a career score, so the whole pool is rankable.' + ) + ); +}; + +const talentPriorities = (f) => { + const t = f.talent; + if (!f.profiles.length) return answer('No talent profiles registered yet.'); + + return doc( + heading('Who to prioritize', 'by career score, availability and verification'), + table( + [ + { key: 'name', label: 'Worker' }, + { key: 'score', label: 'Score', align: 'right' }, + { key: 'experience', label: 'Years', align: 'right' }, + { key: 'availability', label: 'Available' }, + ], + t.ranked.slice(0, 6).map((p) => ({ + name: p.full_name || p.email, + /* The page shows career score on the 300–850 scale, so this must too — + the same worker cannot be 94 here and 817 in the row beside it. */ + score: p.krow_score ? toFICO(p.krow_score) : '—', + experience: p.experience_years ?? '—', + availability: (p.availability || []).length ? (p.availability || []).join(', ') : '—', + })) + ), + insights([ + t.top[0] && { + tone: 'success', + title: `${t.top[0].full_name || t.top[0].email} leads the pool`, + body: `Career score ${toFICO(t.top[0].krow_score || 0)}${t.top[0].experience_years ? `, ${plural(t.top[0].experience_years, 'year')} of experience` : ''}.`, + }, + t.available.length < f.profiles.length && { + tone: 'info', + title: `${t.available.length} of ${f.profiles.length} list availability`, + body: 'The rest cannot be matched to a shift until availability is on the record.', + }, + ]) + ); +}; + +const talentVerification = (f) => { + const t = f.talent; + if (!t.unverified.length) { + return answer(`All ${plural(f.profiles.length, 'profile')} carry at least one verified skill.`); + } + + return doc( + heading(`${plural(t.unverified.length, 'profile')} without verified skills`), + table( + [ + { key: 'name', label: 'Worker' }, + { key: 'score', label: 'Score', align: 'right' }, + { key: 'skills', label: 'Claimed skills', align: 'right' }, + ], + t.unverified.slice(0, 8).map((p) => ({ + name: p.full_name || p.email, + score: p.krow_score ? toFICO(p.krow_score) : '—', + skills: (p.skills || []).length, + })) + ), + insights([{ + tone: 'warning', + title: 'Claimed is not proven', + body: 'These profiles carry skills nobody has verified. A Forge challenge turns each claim into evidence an employer can read.', + }]), + note(`${pct(t.unverified.length, f.profiles.length)}% of the pool is unverified.`) + ); +}; + +const talentAvailability = (f) => { + const t = f.talent; + const buckets = Object.values(f.profiles.reduce((acc, p) => { + (p.availability || ['Not stated']).forEach((slot) => { + acc[slot] ||= { slot, count: 0 }; + acc[slot].count += 1; + }); + return acc; + }, {})).sort((a, b) => b.count - a.count); + + return doc( + heading('Availability across the pool', `${t.available.length} of ${f.profiles.length} stated`), + meters(buckets.map((b) => ({ label: b.slot, value: b.count, max: f.profiles.length }))), + text( + buckets[0] + ? `Deepest cover is **${buckets[0].slot}** at ${plural(buckets[0].count, 'worker')}. Thinnest is **${buckets[buckets.length - 1].slot}**.` + : 'No availability recorded yet.' + ) + ); +}; + +/* ── Hired history ──────────────────────────────────────────────────────── */ + +const hiringOutcomes = (f) => { + const h = f.hiring; + if (!h.hires.length) return answer('Nobody has been hired yet, so there is no history to read.'); + + return doc( + heading('Hiring outcomes', plural(h.hires.length, 'hire')), + kpis([ + { label: 'Hires', value: h.hires.length, sub: `${h.onboarding.length} onboarding` }, + { label: 'Average quality', value: h.avgQuality || '—', sub: 'AI score at hire' }, + { label: 'Average speed', value: h.avgSpeed ? `${h.avgSpeed}d` : '—', sub: 'apply to hire' }, + { label: 'Rated', value: `${h.rated.length}/${h.hires.length}`, sub: 'client rating on file', tone: h.unrated.length ? 'warning' : undefined }, + ]), + heading('By department'), + table( + [ + { key: 'name', label: 'Department' }, + { key: 'count', label: 'Hires', align: 'right' }, + { key: 'avgScore', label: 'Avg score', align: 'right' }, + { key: 'avgDays', label: 'Avg days', align: 'right' }, + ], + h.byDepartment.map((d) => ({ + name: d.name, + count: d.count, + avgScore: d.avgScore || '—', + avgDays: d.avgDays || '—', + })) + ), + h.unrated.length + ? note(`${plural(h.unrated.length, 'hire')} ${verb(h.unrated.length, 'has', 'have')} no client rating yet, so outcome quality rests on the ${h.rated.length} that do.`) + : null + ); +}; + +const hiringStrongest = (f) => { + const h = f.hiring; + if (!h.hires.length) return answer('No hires on record yet.'); + + const ranked = [...h.hires].sort((a, b) => (b.score || 0) - (a.score || 0)); + + return doc( + heading('Strongest hires', 'by score at the point of hire'), + table( + [ + { key: 'name', label: 'Hire' }, + { key: 'role', label: 'Role' }, + { key: 'department', label: 'Department' }, + { key: 'score', label: 'Score', align: 'right' }, + { key: 'rating', label: 'Rating', align: 'right' }, + ], + ranked.slice(0, 6).map((x) => ({ + name: x.name, + role: x.role, + department: x.department, + score: x.score || '—', + rating: x.client_rating || '—', + })) + ), + insights([ + h.strongest && { + tone: 'success', + title: `${h.strongest.name} scored highest at ${h.strongest.score}`, + body: `${h.strongest.role} in ${h.strongest.department}${h.strongest.client_rating ? `, rated ${h.strongest.client_rating} since` : ', not yet rated by the client'}.`, + }, + h.byRoleTitle[0] && { + tone: 'info', + title: `${h.byRoleTitle[0].name} is the most-hired role`, + body: `${plural(h.byRoleTitle[0].count, 'hire')}, averaging ${h.byRoleTitle[0].avgScore || 0} at hire.`, + }, + ]) + ); +}; + +const hiringPatterns = (f) => { + const h = f.hiring; + if (!h.hires.length) return answer('No hires on record, so there is no pattern to read.'); + + const fastest = [...h.byRoleTitle].filter((r) => r.avgDays).sort((a, b) => a.avgDays - b.avgDays)[0]; + const slowest = [...h.byRoleTitle].filter((r) => r.avgDays).sort((a, b) => b.avgDays - a.avgDays)[0]; + + return doc( + heading('What stands out', `${plural(h.hires.length, 'hire')} across ${plural(h.byDepartment.length, 'department')}`), + meters(h.byDepartment.map((d) => ({ + label: d.name, + value: d.count, + max: Math.max(...h.byDepartment.map((x) => x.count), 1), + }))), + insights([ + h.byDepartment[0] && { + tone: 'info', + title: `${h.byDepartment[0].name} accounts for the most hires`, + body: `${plural(h.byDepartment[0].count, 'hire')} of ${h.hires.length} — ${pct(h.byDepartment[0].count, h.hires.length)}% of everyone placed.`, + }, + fastest && slowest && fastest.name !== slowest.name && { + tone: 'warning', + title: `${slowest.name} takes ${slowest.avgDays - fastest.avgDays} days longer than ${fastest.name}`, + body: `${slowest.avgDays} days against ${fastest.avgDays}. The gap is where the process, not the candidate, is the constraint.`, + }, + h.unrated.length && { + tone: 'warning', + title: `${plural(h.unrated.length, 'hire')} without a client rating`, + body: 'Outcome quality cannot be judged on hires nobody has rated.', + }, + ]), + actions([ + h.unrated.length ? { title: 'Chase the missing ratings', body: `${plural(h.unrated.length, 'placement')} ${verb(h.unrated.length, 'has', 'have')} no client feedback on file.` } : null, + h.avgSpeed ? { title: 'Compare speed against quality', body: `Average ${h.avgSpeed} days to hire at an average score of ${h.avgQuality}. Faster is only better while the score holds.` } : null, + ]) + ); +}; + +const hiringRecentHires = (f) => { + const h = f.hiring; + if (!h.hires.length) return answer('No hires on record yet.'); + + const recent = [...h.hires] + .filter((x) => x.hire_date) + .sort((a, b) => new Date(b.hire_date).getTime() - new Date(a.hire_date).getTime()) + .slice(0, 6); + + return doc( + heading('Recent hires', 'most recent first'), + timeline(recent.map((x) => ({ + title: `${x.name} — ${x.role}`, + description: `${x.department}${x.score ? ` · scored ${x.score}` : ''}${x.timeToHire ? ` · ${plural(x.timeToHire, 'day')} to hire` : ''}`, + timestamp: x.hire_date, + tone: x.status === 'onboarding' ? 'warning' : 'success', + }))), + h.onboarding.length + ? note(`${plural(h.onboarding.length, 'hire')} ${verb(h.onboarding.length, 'is', 'are')} still onboarding.`) + : null + ); +}; + +/* ── Free-text responders for the three new contexts ────────────────────── */ + +export const respondForge = (question, f) => { + const q = question.toLowerCase(); + if (has(q, 'block', 'locked', 'lock', 'requirement', 'prerequisite', 'why can', 'unlock')) return forgeBlockers(f); + if (has(q, 'next', 'should i', 'which challenge', 'recommend', 'best')) return forgeNextChallenge(f); + if (has(q, 'ready', 'start now', 'available', 'can i start', 'active challenge')) return forgeReadyNow(f); + if (has(q, 'skill', 'verified', 'proof', 'evidence', 'badge')) return forgeSkillMap(f); + if (has(q, 'summar', 'progress', 'how am i', 'xp', 'overview')) return forgeProgress(f); + return forgeProgress(f); +}; + +export const respondTalentPool = (question, f) => { + const q = question.toLowerCase(); + if (has(q, 'verif', 'unverified', 'proof', 'evidence', 'badge')) return talentVerification(f); + if (has(q, 'available', 'availability', 'shift', 'weekend', 'evening')) return talentAvailability(f); + if (has(q, 'priorit', 'strongest', 'best', 'top', 'who should')) return talentPriorities(f); + if (has(q, 'unscored', 'no score', 'missing', 'gap')) return talentPoolSummary(f); + if (has(q, 'summar', 'overview', 'pool', 'how many')) return talentPoolSummary(f); + return talentPriorities(f); +}; + +export const respondHiredHistory = (question, f) => { + const q = question.toLowerCase(); + if (has(q, 'pattern', 'stand out', 'trend', 'insight', 'attention', 'concern')) return hiringPatterns(f); + if (has(q, 'strongest', 'best', 'top', 'quality', 'highest')) return hiringStrongest(f); + if (has(q, 'recent', 'latest', 'summar', 'who did we hire')) return hiringRecentHires(f); + if (has(q, 'department', 'position', 'role', 'team', 'most hires')) return hiringOutcomes(f); + return hiringOutcomes(f); +}; + +/* ── Capability manifests for the three new contexts ────────────────────── */ + +export const FORGE_CAPABILITIES = [ + { id: 'forge-progress', label: 'Forge Progress', icon: Gauge, run: forgeProgress }, + { id: 'forge-next', label: 'Next Challenge', icon: Target, run: forgeNextChallenge }, + { id: 'forge-ready', label: 'Ready To Start', icon: UserCheck, run: forgeReadyNow }, + { id: 'forge-blockers', label: 'What Is Blocking Me', icon: Lock, run: forgeBlockers }, + { id: 'forge-skills', label: 'Skill Map', icon: ShieldCheck, run: forgeSkillMap }, +]; + +export const TALENT_POOL_CAPABILITIES = [ + { id: 'talent-priorities', label: 'Who To Prioritize', icon: Star, run: talentPriorities }, + { id: 'talent-summary', label: 'Pool Summary', icon: Users, run: talentPoolSummary }, + { id: 'talent-verification', label: 'Verification Gaps', icon: ShieldCheck, run: talentVerification }, + { id: 'talent-availability', label: 'Availability', icon: Clock, run: talentAvailability }, +]; + +export const HIRED_HISTORY_CAPABILITIES = [ + { id: 'hiring-outcomes', label: 'Hiring Outcomes', icon: Award, run: hiringOutcomes }, + { id: 'hiring-strongest', label: 'Strongest Hires', icon: TrendingUp, run: hiringStrongest }, + { id: 'hiring-patterns', label: 'What Stands Out', icon: Layers, run: hiringPatterns }, + { id: 'hiring-recent', label: 'Recent Hires', icon: ScrollText, run: hiringRecentHires }, +]; diff --git a/src/components/ai-assistant/contexts.js b/src/components/ai-assistant/contexts.js index 2e01544..0645e75 100644 --- a/src/components/ai-assistant/contexts.js +++ b/src/components/ai-assistant/contexts.js @@ -4,9 +4,11 @@ import { } from './capabilities/employer'; import { ACTIVITY_CAPABILITIES, ADMIN_ANALYTICS_CAPABILITIES, ADMIN_CANDIDATE_CAPABILITIES, - CANDIDATE_LIST_CAPABILITIES, CONTROL_CENTER_CAPABILITIES, POSITIONS_CAPABILITIES, + CANDIDATE_LIST_CAPABILITIES, CONTROL_CENTER_CAPABILITIES, FORGE_CAPABILITIES, + HIRED_HISTORY_CAPABILITIES, POSITIONS_CAPABILITIES, TALENT_POOL_CAPABILITIES, respondActivity, respondAdminAnalytics, respondAdminCandidates, respondCandidateList, - respondControlCenter, respondPositions, + respondControlCenter, respondForge, respondHiredHistory, respondPositions, + respondTalentPool, } from './capabilities/admin'; /** @@ -74,6 +76,27 @@ export const ASSISTANT_CONTEXTS = { capabilities: ADMIN_ANALYTICS_CAPABILITIES, respond: respondAdminAnalytics, }, + /* Forge, Talent Pool and Hired History read the same platform records as the + contexts above, but ask different questions of them: proof and progression, + supply and readiness, and outcomes after the hire. */ + 'admin.forge': { + id: 'admin.forge', + page: 'KROW Forge', + capabilities: FORGE_CAPABILITIES, + respond: respondForge, + }, + 'admin.talentPool': { + id: 'admin.talentPool', + page: 'Talent Pool', + capabilities: TALENT_POOL_CAPABILITIES, + respond: respondTalentPool, + }, + 'admin.hiredHistory': { + id: 'admin.hiredHistory', + page: 'Hired History', + capabilities: HIRED_HISTORY_CAPABILITIES, + respond: respondHiredHistory, + }, 'admin.activity': { id: 'admin.activity', page: 'Activity', diff --git a/src/components/ai-assistant/dynamic.js b/src/components/ai-assistant/dynamic.js index 0c140f3..138e3bc 100644 --- a/src/components/ai-assistant/dynamic.js +++ b/src/components/ai-assistant/dynamic.js @@ -133,6 +133,38 @@ const GREETINGS = { return `${plural(f.hired.length, 'hire')} from ${plural(f.total, 'applicant')} — ${f.hireRate}% conversion.`; }, + 'admin.forge': (f) => { + const g = f.forge; + if (!g.courses.length) return 'No challenges published yet, so nothing can be proven here.'; + if (g.locked.length && !g.ready.length) { + return `${plural(g.locked.length, 'challenge')} ${verb(g.locked.length, 'is', 'are')} gated by real-work requirements.`; + } + if (!g.verified.length) { + return `Nothing verified yet — ${plural(g.ready.length, 'challenge')} ${verb(g.ready.length, 'is', 'are')} ready to start.`; + } + return `${plural(g.verified.length, 'skill')} verified, ${plural(g.ready.length, 'challenge')} ready to start.`; + }, + + 'admin.talentPool': (f) => { + if (!f.profiles.length) return 'No talent profiles registered yet.'; + if (f.unscoredProfiles.length) { + return `${plural(f.unscoredProfiles.length, 'profile')} ${verb(f.unscoredProfiles.length, 'has', 'have')} no career score and cannot be ranked.`; + } + if (f.talent.unverified.length) { + return `${plural(f.talent.unverified.length, 'profile')} ${verb(f.talent.unverified.length, 'carries', 'carry')} no verified skill.`; + } + return `${plural(f.profiles.length, 'worker')} in the pool, ${f.elite.length} Elite.`; + }, + + 'admin.hiredHistory': (f) => { + const h = f.hiring; + if (!h.hires.length) return 'Nobody has been hired yet, so there is no history to read.'; + if (h.unrated.length) { + return `${plural(h.unrated.length, 'hire')} ${verb(h.unrated.length, 'has', 'have')} no client rating on file.`; + } + return `${plural(h.hires.length, 'hire')} placed, averaging ${h.avgQuality} at hire.`; + }, + 'admin.activity': (f) => { if (!f.activity.length) return 'No activity logged yet.'; const patterns = f.activitySignals.flags.length; @@ -168,6 +200,9 @@ const TITLES = { 'admin.candidates': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`, 'admin.analytics': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`, 'admin.activity': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`, + 'admin.forge': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`, + 'admin.talentPool': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`, + 'admin.hiredHistory': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`, }; /** Descriptions state what this page's assistant can actually do with the data. */ @@ -187,6 +222,12 @@ const DESCRIPTIONS = { 'admin.analytics': (f) => `Reading hiring performance across ${plural(f.openPositions.length, 'position')}, ${plural(f.total, 'candidate')} and ${plural(new Set(f.byRole.map((r) => r.category)).size, 'department')} — conversion, speed and outcomes.`, 'admin.activity': (f) => `${plural(f.activity.length, 'logged event')}, reconciled and profiled.`, + 'admin.forge': (f) => + `Reading ${plural(f.forge.courses.length, 'challenge')} — verified skills, what is ready to start and what is still locked.`, + 'admin.talentPool': (f) => + `Reading ${plural(f.profiles.length, 'talent profile')} — availability, career score and verification.`, + 'admin.hiredHistory': (f) => + `Reading ${plural(f.hiring.hires.length, 'hire')} across ${plural(f.hiring.byDepartment.length, 'department')} — quality, speed and outcomes.`, }; /** @@ -242,6 +283,21 @@ const PLACEHOLDERS = { 'Ask Owliver about unusual activity…', 'Ask Owliver about security concerns…', ], + 'admin.forge': () => [ + 'Ask Owliver which challenge to take next…', + 'Ask Owliver what is blocking a skill…', + 'Ask Owliver about Forge progress…', + ], + 'admin.talentPool': () => [ + 'Ask Owliver who to prioritize…', + 'Ask Owliver about verification gaps…', + 'Ask Owliver who is available…', + ], + 'admin.hiredHistory': () => [ + 'Ask Owliver about hiring outcomes…', + 'Ask Owliver which roles hired best…', + 'Ask Owliver what stands out…', + ], }; /* ── Suggested prompts ──────────────────────────────────────────────────── */ @@ -365,6 +421,58 @@ const PROMPTS = { : { label: 'What needs attention?', prompt: 'What needs attention?', capability: 'attention-required' }, ], + /* Forge, Talent Pool and Hired History. Same rule as the contexts above: the + first chip is whatever the data has made urgent, so the set reorders itself + rather than offering the same questions regardless of state. */ + 'admin.forge': (f) => { + const g = f.forge; + return [ + g.next + ? { label: `Should I take ${g.next.title}?`, prompt: `Which challenge should I complete next? Start with ${g.next.title}`, capability: 'forge-next' } + : { label: 'Which challenge should I take next?', prompt: 'Which challenge should I complete next?', capability: 'forge-next' }, + g.locked.length + ? { label: `${g.locked.length} blocked by requirements`, prompt: 'What is blocking my next skill?', capability: 'forge-blockers' } + : { label: 'What is blocking my next skill?', prompt: 'What is blocking my next skill?', capability: 'forge-blockers' }, + g.ready.length + ? { label: `${g.ready.length} ready to start`, prompt: 'Which challenges can I start right now?', capability: 'forge-ready' } + : { label: 'Which challenges can I start?', prompt: 'Which challenges can I start right now?', capability: 'forge-ready' }, + { label: 'Which skills am I closest to unlocking?', prompt: 'Which skills am I closest to unlocking?', capability: 'forge-skills' }, + { label: 'Summarize my Forge progress', prompt: 'Summarize my Forge progress', capability: 'forge-progress' }, + ]; + }, + + 'admin.talentPool': (f) => { + const t = f.talent; + return [ + t.top[0] + ? { label: `Why does ${t.top[0].full_name?.split(' ')[0] || 'the top profile'} rank first?`, prompt: 'Which talent should I prioritize?', capability: 'talent-priorities' } + : { label: 'Which talent should I prioritize?', prompt: 'Which talent should I prioritize?', capability: 'talent-priorities' }, + f.unscoredProfiles.length + ? { label: `${f.unscoredProfiles.length} profiles unscored`, prompt: 'Which profiles are unscored?', capability: 'talent-summary' } + : { label: 'Summarize the talent pool', prompt: 'Summarize the talent pool', capability: 'talent-summary' }, + t.unverified.length + ? { label: `${t.unverified.length} need verification`, prompt: 'Which talent needs verification?', capability: 'talent-verification' } + : { label: 'Which talent needs verification?', prompt: 'Which talent needs verification?', capability: 'talent-verification' }, + { label: 'Who is currently available?', prompt: 'Who is currently available?', capability: 'talent-availability' }, + { label: 'Who has the strongest score?', prompt: 'Who has the strongest score?', capability: 'talent-priorities' }, + ]; + }, + + 'admin.hiredHistory': (f) => { + const h = f.hiring; + return [ + h.unrated.length + ? { label: `${h.unrated.length} hires unrated`, prompt: 'What needs attention in hiring outcomes?', capability: 'hiring-patterns' } + : { label: 'What hiring patterns stand out?', prompt: 'What hiring patterns stand out?', capability: 'hiring-patterns' }, + h.byDepartment[0] + ? { label: `${h.byDepartment[0].name} leads on hires`, prompt: 'Which department has the most hires?', capability: 'hiring-outcomes' } + : { label: 'Which department has the most hires?', prompt: 'Which department has the most hires?', capability: 'hiring-outcomes' }, + { label: 'Which positions hired the strongest talent?', prompt: 'Which positions hired the strongest talent?', capability: 'hiring-strongest' }, + { label: 'Summarize recent hires', prompt: 'Summarize recent hires', capability: 'hiring-recent' }, + { label: 'What hiring outcomes should I review?', prompt: 'What hiring outcomes should I review?', capability: 'hiring-outcomes' }, + ]; + }, + 'admin.activity': (f) => [ f.activitySignals.flags.length ? { label: `${f.activitySignals.flags.length} unusual patterns`, prompt: 'Show unusual activity on the platform', capability: 'unusual-activity' } diff --git a/src/components/ai-assistant/insights.js b/src/components/ai-assistant/insights.js index 19451d0..ebd9651 100644 --- a/src/components/ai-assistant/insights.js +++ b/src/components/ai-assistant/insights.js @@ -9,6 +9,9 @@ * transport, no dependency on any AI product. */ +import { isUnlocked } from '@/lib/provingGround'; +import { getScoreBand } from '@/lib/talentHome'; + export const pct = (n, d) => (d ? Math.round((n / d) * 100) : 0); export const plural = (n, word, irregular) => @@ -174,6 +177,107 @@ export function buildFacts({ }; })(); + /* ── KROW Forge ────────────────────────────────────────────────────────── + Derived with the same gate (`isUnlocked`) and the same completion record the + Forge page renders from, so the panel and the page cannot disagree about + what is ready, what is blocked, or why. `profile` is the signed-in worker's + record when one is already in cache; without it the counts are the library's + rather than the person's, which is still true — just less personal. */ + const forgeCourses = courses.filter((c) => c.status === 'active'); + const forgeCompletedIds = new Set((profile?.completed_courses || []).map((c) => c.course_id)); + const forgeGate = (course) => isUnlocked(course, profile || {}); + const forgeUnfinished = forgeCourses.filter((c) => !forgeCompletedIds.has(c.id)); + const forgeReady = forgeUnfinished.filter((c) => forgeGate(c).unlocked); + const forgeLocked = forgeUnfinished.filter((c) => !forgeGate(c).unlocked); + const forge = { + courses: forgeCourses, + verified: profile?.earned_badges || [], + completed: (profile?.completed_courses || []).filter((c) => c.status !== 'in_progress'), + inProgress: (profile?.completed_courses || []).filter((c) => c.status === 'in_progress'), + ready: forgeReady, + locked: forgeLocked, + /* The page features the first unfinished challenge whether or not it is + unlocked, so the panel names the same one — a chip that recommends a + different challenge than the one on screen reads as a second opinion. */ + next: forgeUnfinished[0] || null, + xpAvailable: forgeUnfinished.reduce((sum, c) => sum + (c.xp || 0), 0), + /* Every blocked challenge with the reasons that block it, in page order. */ + blocked: forgeLocked.map((course) => ({ course, reasons: forgeGate(course).reasons })), + byDifficulty: ['beginner', 'intermediate', 'advanced'].map((level) => ({ + level, + count: forgeCourses.filter((c) => c.difficulty === level).length, + })).filter((d) => d.count), + }; + + /* ── Talent pool ───────────────────────────────────────────────────────── + The same score bands the Talent Pool page labels profiles with. */ + const rankedProfiles = [...profiles].sort((a, b) => (b.krow_score || 0) - (a.krow_score || 0)); + const talent = { + ranked: rankedProfiles, + top: rankedProfiles.slice(0, 3), + available: profiles.filter((p) => (p.availability || []).length > 0), + unverified: profiles.filter( + (p) => !(p.earned_badges || []).length + && !(p.capabilities || []).some((c) => c.status === 'verified') + ), + experienced: profiles.filter((p) => (p.experience_years || 0) >= 3), + avgScore: avg(profiles.filter((p) => p.krow_score > 0).map((p) => p.krow_score)), + bands: Object.values(profiles.reduce((acc, p) => { + const { label } = getScoreBand(p.krow_score || 0); + acc[label] ||= { label, count: 0 }; + acc[label].count += 1; + return acc; + }, {})).sort((a, b) => b.count - a.count), + }; + + /* ── Hired history ─────────────────────────────────────────────────────── + Staff joined to the application and posting they came from — the same join + the Hired History page performs, so department, speed and score match. */ + const hires = staff.map((s) => { + const app = applications.find((a) => a.id === s.application_id); + const posting = postings.find((p) => p.id === s.job_posting_id); + return { + ...s, + department: posting?.role_category || '—', + timeToHire: app + ? Math.max(1, Math.round( + (new Date(app.updated_date).getTime() - new Date(app.created_date).getTime()) / DAY_MS + )) + : null, + score: s.ai_score || app?.ai_score || 0, + }; + }); + + const groupHires = (key) => Object.values(hires.reduce((acc, h) => { + const name = h[key] || 'Unspecified'; + acc[name] ||= { name, count: 0, scores: [], days: [], ratings: [] }; + acc[name].count += 1; + if (h.score) acc[name].scores.push(h.score); + if (h.timeToHire) acc[name].days.push(h.timeToHire); + if (h.client_rating) acc[name].ratings.push(h.client_rating); + return acc; + }, {})).map((e) => ({ + name: e.name, + count: e.count, + avgScore: avg(e.scores), + avgDays: avg(e.days), + avgRating: e.ratings.length + ? Number((e.ratings.reduce((a, b) => a + b, 0) / e.ratings.length).toFixed(1)) + : null, + })).sort((a, b) => b.count - a.count); + + const hiring = { + hires, + byDepartment: groupHires('department'), + byRoleTitle: groupHires('role'), + unrated: hires.filter((h) => !h.client_rating), + rated: hires.filter((h) => h.client_rating), + avgQuality: avg(hires.map((h) => h.score).filter(Boolean)), + avgSpeed: avg(hires.map((h) => h.timeToHire).filter(Boolean)), + strongest: [...hires].sort((a, b) => (b.score || 0) - (a.score || 0))[0] || null, + onboarding: hires.filter((h) => h.status === 'onboarding'), + }; + return { today, @@ -248,9 +352,12 @@ export function buildFacts({ eventCounts, activitySignals, - /* Signed-in worker (talent context only) */ + /* Signed-in worker, and the pages built on these records */ profile, courses, + forge, + talent, + hiring, /* Economics — mirrors ImpactMetrics */ costSaved: scored.length * 45 + interviews.length * 80, diff --git a/src/components/ai-assistant/placement.js b/src/components/ai-assistant/placement.js index 860127e..0e62913 100644 --- a/src/components/ai-assistant/placement.js +++ b/src/components/ai-assistant/placement.js @@ -19,6 +19,9 @@ const PLACEMENT = { '/admin/candidates-analysis': 'admin.candidates', '/admin/analytics': 'admin.analytics', '/admin/activity': 'admin.activity', + '/admin/university': 'admin.forge', + '/admin/talent-pool': 'admin.talentPool', + '/admin/hired': 'admin.hiredHistory', }, }; @@ -26,9 +29,9 @@ const PLACEMENT = { * Pages that must never carry the assistant, listed explicitly so the intent is * documented rather than implied by omission. * - * Admin: the reporting and reference pages, plus login. The working surfaces — - * Control Center, Positions, Candidates, Candidates Analysis, Analytics - * and Activity — all carry the panel. + * Admin: profile and login only. Every working surface — Control Center, + * Positions, Candidates, Candidates Analysis, Analytics, Activity, + * KROW Forge, Talent Pool and Hired History — carries the panel. * Employer: every page. The contextual panel is an Admin capability. * Talent: every page. The talent portal has the standalone Owliver product, * with its own voice experience, branding and workflow. @@ -39,8 +42,8 @@ export const EXCLUDED_ROUTES = [ '/hired', '/talent-pool', '/analytics', '/profile', '/apply', '/university', '/university/:id', '/me', '/identity', '/employee', '/owliver', '/design-system', - // Admin pages that are a report or a form rather than a workflow. - '/admin/hired', '/admin/talent-pool', '/admin/profile', '/admin/login', + // Admin pages that are a form rather than a workflow. + '/admin/profile', '/admin/login', // The full candidate profile is one person's record, not a page-level question. '/admin/candidates/:id', ]; diff --git a/src/components/ai-assistant/useAssistant.js b/src/components/ai-assistant/useAssistant.js index 0b51c47..41f6890 100644 --- a/src/components/ai-assistant/useAssistant.js +++ b/src/components/ai-assistant/useAssistant.js @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query'; import { base44 } from '@/api/base44Client'; import { useApplications, useCourses, useInterviews, useJobPostings, useStaff, - useUserActivity, useWorkerProfiles, + useUserActivity, useWorkerProfile, useWorkerProfiles, } from '@/lib/krowHooks'; import { buildFacts } from './insights'; import { createAssistantProvider } from './provider'; @@ -25,12 +25,16 @@ export function useAssistantFacts() { const { data: activity = [] } = useUserActivity(); const { data: courses = [] } = useCourses(); - // Krow AI serves Employer and Admin only, so it never reads the signed-in - // worker's own profile — that query also creates the record if absent, and it - // belongs to the talent portal, not here. + // The signed-in worker's own record, read from cache only: `enabled: false` + // means this never runs the query, which would *create* a WorkerProfile for an + // admin who has never opened Forge. So the Forge context is personal on pages + // that already loaded it, and falls back to library-level facts elsewhere + // rather than manufacturing a record as a side effect of opening a panel. + const { data: profile = null } = useWorkerProfile({ enabled: false }); + return React.useMemo( - () => buildFacts({ applications, postings, interviews, staff, profiles, activity, courses }), - [applications, postings, interviews, staff, profiles, activity, courses] + () => buildFacts({ applications, postings, interviews, staff, profiles, activity, courses, profile }), + [applications, postings, interviews, staff, profiles, activity, courses, profile] ); } diff --git a/src/components/charts/DepartmentPerformance.jsx b/src/components/charts/DepartmentPerformance.jsx new file mode 100644 index 0000000..79e93a2 --- /dev/null +++ b/src/components/charts/DepartmentPerformance.jsx @@ -0,0 +1,93 @@ +import React from 'react'; +import { ScatterChart } from '@mui/x-charts'; +import { cn } from '@/lib/utils'; +import { chartSx, TONES, tooltipSx } from './muiChartTheme'; + +/** + * Department performance — a dot plot, not a bar race. + * + * The bars this replaces encoded hire *count*, which on a dataset where every + * department has hired once drew three identical full-width rectangles. Encoding + * average score instead does not fix it either: 92, 94 and 97 against a + * zero-baseline axis are three more identical bars. + * + * So the scores are plotted as points on a focused axis. The domain starts below + * the lowest score rather than at zero — legitimate here because the axis ticks + * are drawn and read, and because the question is "which department hires + * better", which is a comparison between 90s, not a comparison against nothing. + * + * Volume is stated in the list underneath: one hire per department is a fact, + * not a shape. + * + * `items: [{ name, count, avgScore }]` — supplied by the page. + */ +export function DepartmentPerformance({ items = [], className }) { + if (!items.length) return null; + + const scored = items.filter((d) => d.avgScore > 0); + /* Highest at the top: MUI plots ascending y upward, so rank ascending. */ + const plotted = [...scored].sort((a, b) => (a.avgScore || 0) - (b.avgScore || 0)); + + const lowest = Math.min(...plotted.map((d) => d.avgScore)); + const highest = Math.max(...plotted.map((d) => d.avgScore)); + /* A little air either side, snapped to 5s, so the ticks stay round numbers. */ + const min = Math.max(0, Math.floor((lowest - 6) / 5) * 5); + const max = Math.min(100, Math.ceil((highest + 3) / 5) * 5); + + return ( +
+ No scores recorded against these hires yet. +
+ )} + +{stage.label}
++ {stage.count} +
+ ++ + {i === 0 ? `${Math.round((stage.count / total) * 100)}% entered` : `${rate}% through`} + + {lost > 0 && −{lost}} +
++ Practical challenges that turn a claim into verified evidence. +
++ {results.length} of {courses.length} challenges{filtered ? ' · filtered' : ''} +
+ {filtered && ( + + )} +No challenges match your filters.
+ ++ Workforce skill verification +
++ Prove real skills through real-world challenges and build a verified workforce reputation. +
++ Next skill to prove +
+ + · {locked ? 'Locked' : 'Ready to start'} + ++ {course.proof_skill || course.category} + · {course.difficulty} +
+ ++ {course.description} +
+What you’ll prove
+How you’ll prove it
+ ++ + {TYPE_LABEL[type]} +
+{TYPE_HINT[type]}
+ + {/* Your action → Owliver → result. The existing evaluation relationship, + drawn as a sequence rather than three tiles. */} ++ + Locked for now +
++ {outstanding === 1 + ? 'One requirement left to unlock this challenge.' + : `${outstanding} requirements left to unlock this challenge.`} +
+ + {/* Capped: at full shell width the label and its meter would sit a + thousand pixels apart and stop reading as one row. */} ++ {course.estimated_minutes} min · {course.xp} XP + {course.badge_reward && ` · Earns ${course.badge_reward}`} +
+ + + {actionLabel} + + ++ {empty + ? 'Your first verified skill starts with a real-world challenge.' + : `${evidenceCount} ${evidenceCount === 1 ? 'piece' : 'pieces'} of evidence on file`} +
++ {skills.length}{' '} + {skills.length === 1 ? 'verified skill' : 'verified skills'} +
+ + {empty ? ( + /* How a skill becomes verified — one line, four steps. */ +- Build verified skills through real-world challenges, strengthen your workforce reputation, and unlock your next opportunities. -
- {firstActiveCourse && ( -{verifiedCount}
-Verified skills
-{inProgressCount}
-In progress
-{nextUnlockCount}
-Next unlock
-Skills you have already proven through KROW Forge.
-- Your Forge profile starts with your first proven skill. Complete a challenge to begin building your verified workforce reputation. -
- -Verified · {badge.level || 'Standard'}
-- Verified {badge.earned_date || 'Jul 2026'} -
-KROW recommends these based on your current skills, experience, and workforce goals.
-{c.description}
- - {!lock.unlocked && lock.reasons?.length > 0 && ( -- Next best skill: Team Leadership. Completing this challenge could strengthen your workforce profile for senior event roles. -
-Complete practical challenges to prove skills and strengthen your verified workforce profile.
-No challenges match your filters.
- -Not enough history for a trend @@ -166,46 +163,15 @@ export default function AdminHiredHistory() { : 'No hires recorded yet. Hiring volume over time appears here once the first role closes.'}