From 3beb66059b3e7bf1d06fedf46a73fe94bfaeedcb Mon Sep 17 00:00:00 2001 From: Aravind Date: Wed, 12 Aug 2026 18:56:56 +0530 Subject: [PATCH] update ui design for all pages --- package-lock.json | 1178 +++++++++++++++-- package.json | 16 +- src/App.jsx | 16 + src/api/base44Client.js | 18 + src/api/seed.js | 7 + .../ai-assistant/AssistantPanel.jsx | 2 +- .../ai-assistant/AssistantPanelContext.jsx | 11 +- src/components/ai-assistant/KrowAssistant.jsx | 146 +- .../ai-assistant/capabilities/admin.js | 410 ++++-- src/components/ai-assistant/contexts.js | 38 +- src/components/ai-assistant/dynamic.js | 84 +- src/components/ai-assistant/insights.js | 36 + src/components/ai-assistant/placement.js | 18 +- src/components/ai-assistant/routing.js | 393 ++++++ src/components/ai-assistant/useAssistant.js | 175 ++- .../charts/DepartmentPerformance.jsx | 680 ++++++++-- src/components/ds/DataTable.jsx | 16 +- src/components/ds/MetricStrip.jsx | 159 ++- src/components/forge/AddSkillTraining.jsx | 522 ++++++++ src/components/forge/ForgeHeader.jsx | 62 +- src/components/forge/ForgeInsight.jsx | 37 +- src/components/forge/ForgeProgress.jsx | 168 +-- src/components/forge/ForgeRail.jsx | 117 ++ src/components/forge/SkillCard.jsx | 173 +++ src/components/forge/SkillDetail.jsx | 184 +++ src/components/forge/SkillLibrary.jsx | 171 +++ src/components/forge/YourForge.jsx | 211 +-- src/components/forge/challengeMeta.js | 82 ++ src/components/forge/index.js | 45 +- src/components/krow/CandidateCard.jsx | 2 +- src/components/krow/UniversityCourseCard.jsx | 2 +- src/components/krow/WorkforceReputation.jsx | 201 ++- .../krow/talent/CareerScoreGauge.jsx | 64 +- .../krow/talent/IdentityCareerCard.jsx | 64 +- src/components/skills/AddSkillDialog.jsx | 189 +++ src/components/ui/calendar.jsx | 59 +- src/index.css | 92 ++ src/layouts/AdminLayout.jsx | 8 +- src/lib/krowHooks.js | 81 ++ src/lib/positionModel.js | 90 ++ src/lib/roleCategories.js | 9 + src/lib/skills/actions.js | 490 +++++++ src/lib/skills/positionFlow.js | 455 +++++++ src/lib/skills/registry.js | 254 ++++ src/pages/CreatePosition.jsx | 87 +- src/pages/University.jsx | 396 +++--- src/pages/admin/Analytics.jsx | 384 +----- src/pages/admin/ControlCenter.jsx | 343 +++-- src/pages/admin/HiredHistory.jsx | 621 +++++++-- src/pages/admin/Positions.jsx | 130 +- src/pages/admin/Profile.jsx | 194 ++- src/pages/admin/TalentPool.jsx | 165 ++- src/pages/admin/_shell.jsx | 18 +- src/pages/admin/permissions.js | 28 + src/skills/analytics/analytics-insights.md | 27 + src/skills/candidates/candidate-search.md | 29 + src/skills/forge/forge-skill-management.md | 64 + src/skills/positions/create-position.md | 62 + 58 files changed, 7892 insertions(+), 1891 deletions(-) create mode 100644 src/components/ai-assistant/routing.js create mode 100644 src/components/forge/AddSkillTraining.jsx create mode 100644 src/components/forge/ForgeRail.jsx create mode 100644 src/components/forge/SkillCard.jsx create mode 100644 src/components/forge/SkillDetail.jsx create mode 100644 src/components/forge/SkillLibrary.jsx create mode 100644 src/components/skills/AddSkillDialog.jsx create mode 100644 src/lib/positionModel.js create mode 100644 src/lib/roleCategories.js create mode 100644 src/lib/skills/actions.js create mode 100644 src/lib/skills/positionFlow.js create mode 100644 src/lib/skills/registry.js create mode 100644 src/pages/admin/permissions.js create mode 100644 src/skills/analytics/analytics-insights.md create mode 100644 src/skills/candidates/candidate-search.md create mode 100644 src/skills/forge/forge-skill-management.md create mode 100644 src/skills/positions/create-position.md diff --git a/package-lock.json b/package-lock.json index dcfd227..a9bb66a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "krow-demo", "version": "0.0.0", "dependencies": { + "@astryxdesign/cli": "^0.3.0", + "@astryxdesign/core": "^0.3.0", + "@astryxdesign/theme-neutral": "^0.3.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^4.1.2", @@ -40,6 +43,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@stylexjs/stylex": "^0.19.0", "@tanstack/react-query": "^5.84.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -52,15 +56,15 @@ "lucide-react": "^0.475.0", "moment": "^2.30.1", "next-themes": "^0.4.4", - "react": "^18.2.0", - "react-day-picker": "^8.10.1", - "react-dom": "^18.2.0", + "react": "^19.2.8", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.8", "react-hook-form": "^7.54.2", "react-hot-toast": "^2.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^2.1.7", "react-router-dom": "^6.26.0", - "recharts": "^2.15.4", + "recharts": "^3.10.1", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", @@ -70,8 +74,8 @@ "devDependencies": { "@eslint/js": "^9.19.0", "@types/node": "^22.13.5", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "eslint": "^9.19.0", @@ -98,6 +102,108 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@astryxdesign/cli": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astryxdesign/cli/-/cli-0.3.0.tgz", + "integrity": "sha512-iLFJsBlZUb66GskA1r3AmuodW72yu0PhCRAorcGPMuwHbGYn54pXGb/EnnWSdHf3Yuw4E3OuN7r4dY0TYJuYmQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "commander": "^12.1.0", + "jiti": "^2.7.0", + "jscodeshift": "^17.3.0", + "zod": "^4.4.3" + }, + "bin": { + "astryx": "clients/cli/bin/astryx.mjs", + "cli": "clients/cli/bin/astryx.mjs" + }, + "peerDependencies": { + "@astryxdesign/charts": "*", + "@astryxdesign/core": "*", + "@astryxdesign/lab": "*", + "@astryxdesign/theme-neutral": "*", + "gpt-tokenizer": "^3.4.0" + }, + "peerDependenciesMeta": { + "@astryxdesign/charts": { + "optional": true + }, + "@astryxdesign/core": { + "optional": true + }, + "@astryxdesign/lab": { + "optional": true + }, + "@astryxdesign/theme-neutral": { + "optional": true + } + } + }, + "node_modules/@astryxdesign/cli/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@astryxdesign/cli/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@astryxdesign/cli/node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@astryxdesign/core": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astryxdesign/core/-/core-0.3.0.tgz", + "integrity": "sha512-t2hHvR7AcwW0oKrnxCwbdEEE5Ytgy0Ug61DcYAl9BxAuV5z4XL0/kMo5DiPvX9jbDxzysmOa8ClnXqzxouRUVQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "intl-messageformat": "^11.2.9" + }, + "peerDependencies": { + "@stylexjs/stylex": "^0.19.0", + "react": ">=19.0.0", + "react-dom": ">=19.0.0" + } + }, + "node_modules/@astryxdesign/theme-neutral": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@astryxdesign/theme-neutral/-/theme-neutral-0.3.0.tgz", + "integrity": "sha512-CeKRfmTEtww60WZK4pSlW7bheBNyVPpgEliiXXUHRStXgcXOhwiUA3D/cXhMJlEx+VRgVxnzl0hMbWIr42RaVQ==", + "license": "MIT", + "dependencies": { + "lucide-react": "^1.18.0" + }, + "peerDependencies": { + "@astryxdesign/core": "0.3.0", + "react": ">=19" + } + }, + "node_modules/@astryxdesign/theme-neutral/node_modules/lucide-react": { + "version": "1.31.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.31.0.tgz", + "integrity": "sha512-G8u2eEtoHUnUa9f8lbvqDhCiORMnYLdUEo06EEG9MQvHQrInKcX3Pa2TH39MM5qyzRcWETxB0+aOwAPI1g1kEg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -116,7 +222,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -126,7 +231,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.29.7", @@ -169,11 +273,22 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", - "dev": true, "license": "MIT", "dependencies": { "@babel/compat-data": "^7.29.7", @@ -186,6 +301,27 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -195,6 +331,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -212,7 +361,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.29.7", @@ -226,16 +374,57 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -258,7 +447,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -268,7 +456,6 @@ "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.29.7", @@ -293,6 +480,146 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.29.7.tgz", + "integrity": "sha512-ajMX6QPcyomotqwpzhkYGxcK2i/us0rs1Qo9QvUpa+Fca0FTmqrzKrctoIYLMxcOhGZldGT/BAVkRGTWBiR8gQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", + "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.29.7.tgz", + "integrity": "sha512-wRHeUjUjCZnMHmiO5bRgjFLcoEh7JyTdByOW11ahhwNa4V0bmeGEaIvt51yq0zQp2yWIpqfxXXPyUP6GFJZHOQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-flow": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", + "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", + "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", + "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-transform-react-jsx-self": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", @@ -325,6 +652,80 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-flow": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-flow/-/preset-flow-7.29.7.tgz", + "integrity": "sha512-KYIRV0BuaN68CDdsqFkAD7MU7yipUqQNuNElwATdxaIdpTjhvtY82QvkBJs7zV3Evxj2jFAAZ1iO8nyy0nhjqA==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-transform-flow-strip-types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz", + "integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "@babel/plugin-syntax-jsx": "^7.29.7", + "@babel/plugin-transform-modules-commonjs": "^7.29.7", + "@babel/plugin-transform-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/register": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/register/-/register-7.29.7.tgz", + "integrity": "sha512-AMGJoWuES861riy6pcB0fphE1YXybtQnBYQMuIyPv6mKLiosfa79BKTnAOyx215c/3RJPJpdQwoHZ3earVH7AA==", + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "find-cache-dir": "^2.0.0", + "make-dir": "^2.1.0", + "pirates": "^4.0.6", + "source-map-support": "^0.5.16" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", @@ -401,6 +802,12 @@ } } }, + "node_modules/@date-fns/tz": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.5.0.tgz", + "integrity": "sha512-lwYN/vDPeNRULcepoE/LO2Pgx+7/RV+S9ARfbc9lr2DtGkOD7pAiruHvbR1RX3Qyf6ja47EWJDMsNK5vK08DJg==", + "license": "MIT" + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -1190,6 +1597,27 @@ "integrity": "sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==", "license": "MIT" }, + "node_modules/@formatjs/fast-memoize": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.7.tgz", + "integrity": "sha512-zXfhLpvA6T7+efdt9JLbBwZ00tT7NsBMDVnDu8rpHeNNv8KfRZAMo2gkG0k9lK/Nzc//3kJ9pImsfuJxk3KhUA==", + "license": "MIT" + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "3.5.16", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-3.5.16.tgz", + "integrity": "sha512-kl6b/4D56gjGZi4ZewSmvXbalHwjOUI5ogEHPZqw42goeXTTrL7/yuPzvdrvr0QigDtvaOeb+UeMf62jks43Yg==", + "license": "MIT", + "dependencies": { + "@formatjs/icu-skeleton-parser": "2.1.11" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-2.1.11.tgz", + "integrity": "sha512-j8cUmOJzVgkHuS0QiQ6ga76UIoLOFSAMWhs7aZJztH3aAdCOAE6vpC8KVvFB4cU10ON0y2/5oOVmPJ43s2lTwA==", + "license": "MIT" + }, "node_modules/@hookform/resolvers": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.3.tgz", @@ -1282,7 +1710,6 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -3032,6 +3459,32 @@ "integrity": "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw==", "license": "MIT" }, + "node_modules/@reduxjs/toolkit": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.12.0.tgz", + "integrity": "sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@remix-run/router": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.3.tgz", @@ -3398,12 +3851,38 @@ "win32" ] }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@standard-schema/utils": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@stylexjs/stylex": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@stylexjs/stylex/-/stylex-0.19.0.tgz", + "integrity": "sha512-CnUFp7YMaDLDeemsWOfJgoC/gKM5P/yBNMcpJaE6ChJmXr7s0DJwSeGTTlHJcqqwN9OW1qGtmARWLFhGZN1pTA==", + "license": "MIT", + "dependencies": { + "css-mediaquery": "^0.1.2", + "invariant": "^2.2.4", + "styleq": "0.2.1" + } + }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@tanstack/query-core": { "version": "5.101.4", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.101.4.tgz", @@ -3643,23 +4122,22 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.7", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", - "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "devOptional": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/react-transition-group": { @@ -3677,6 +4155,12 @@ "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@ungap/structured-clone": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz", @@ -3942,6 +4426,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ast-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/async-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", @@ -4071,7 +4567,6 @@ "version": "2.11.12", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", - "dev": true, "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -4125,7 +4620,6 @@ "version": "4.28.7", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4155,6 +4649,12 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "license": "MIT" + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -4227,7 +4727,6 @@ "version": "1.0.30001809", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", - "dev": true, "funding": [ { "type": "opencollective", @@ -4359,6 +4858,20 @@ "url": "https://polar.sh/cva" } }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -4423,6 +4936,12 @@ "node": ">= 6" } }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "license": "MIT" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -4434,7 +4953,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, "license": "MIT" }, "node_modules/cosmiconfig": { @@ -4486,6 +5004,12 @@ "utrie": "^1.0.2" } }, + "node_modules/css-mediaquery": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", + "integrity": "sha512-COtn4EROW5dBGlE/4PiKnh6rZpAPxDeFLaEEwt4i10jpDMFt2EhQGS79QmmrO+iKCHv0PU/HrOWEhijFd1x99Q==", + "license": "BSD" + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4701,6 +5225,12 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/date-fns-jalali": { + "version": "4.1.0-0", + "resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz", + "integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -4862,7 +5392,6 @@ "version": "1.5.402", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.402.tgz", "integrity": "sha512-/oOpMaPT6Yg+6/1XQhyIPlzgj7Ye9zf+nNM2Uh6OcE2G2oNptWazFa+qB2Pdqqbsc9KnIDzgAntoYN0dbwOXwA==", - "dev": true, "license": "ISC" }, "node_modules/embla-carousel": { @@ -5100,6 +5629,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esbuild": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", @@ -5146,7 +5686,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -5344,6 +5883,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -5401,9 +5953,9 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "license": "MIT" }, "node_modules/extend": { @@ -5419,15 +5971,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-equals": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.1.tgz", - "integrity": "sha512-DjlFSM5Pk9cGcL0q5QXl66eGzx0N6szNgaswwc5ZphlBohjTVJSnGgI+rJVOgOi65qUoQnDZN4nDqi33udtydQ==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/fast-glob": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", @@ -5504,6 +6047,20 @@ "node": ">=8" } }, + "node_modules/find-cache-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", + "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "license": "MIT", + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/find-root": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", @@ -5554,6 +6111,27 @@ "dev": true, "license": "ISC" }, + "node_modules/flow-estree": { + "version": "0.326.0", + "resolved": "https://registry.npmjs.org/flow-estree/-/flow-estree-0.326.0.tgz", + "integrity": "sha512-43Qv+Ei9qfabhLx8JGEJku4frHGD5zI2EuL73Hs9HrWqPMbTT/DS18Az7bcodGwdQEv1DVIEI2WowkEbMIk4BQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/flow-parser": { + "version": "0.326.0", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.326.0.tgz", + "integrity": "sha512-H/Wqt2SDkQ8GH8wpyAj434zN40zomipZWBbKYlAwQYyNdMIQMKqpXTx82FMnITt2iDQ5zrV80NvSPAgIAafwGA==", + "license": "MIT", + "dependencies": { + "flow-estree": "0.326.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -5682,7 +6260,6 @@ "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -5818,6 +6395,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gpt-tokenizer": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/gpt-tokenizer/-/gpt-tokenizer-3.4.0.tgz", + "integrity": "sha512-wxFLnhIXTDjYebd9A9pGl3e31ZpSypbpIJSOswbgop5jLte/AsZVDvjlbEuVFlsqZixVKqbcoNmRlFDf6pz/UQ==", + "license": "MIT", + "peer": true + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -5993,6 +6583,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "11.1.16", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.16.tgz", + "integrity": "sha512-Xs7H9rBc+kti1J6RueUvbEBkmOz7jqj11XYgf+YMXAYzu8EeE7hwZ9poLXdVfVnGmJu7QAf41T7H2KuF6QoK6Q==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -6013,7 +6613,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.8.19" @@ -6059,6 +6658,25 @@ "node": ">=12" } }, + "node_modules/intl-messageformat": { + "version": "11.2.13", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-11.2.13.tgz", + "integrity": "sha512-JaPaE6TIX+TAS5XLhDUh41geLw4QfBHX4s5pW8Km+L9fVC8HzB9yOuhbh4EMR/F1+8C6b9qk4763Cv+LdOG1kg==", + "license": "BSD-3-Clause", + "dependencies": { + "@formatjs/fast-memoize": "3.1.7", + "@formatjs/icu-messageformat-parser": "3.5.16" + } + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -6392,6 +7010,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -6551,6 +7181,15 @@ "dev": true, "license": "ISC" }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -6607,6 +7246,58 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jscodeshift": { + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/jscodeshift/-/jscodeshift-17.4.0.tgz", + "integrity": "sha512-i3ESKiiTsGynxzTg5BhsZViD0ai72/6SsI1efDZxG6/5KCoElsmquxtyhXK5lpEgoO7MTNpYkjFEdEL97SkBNg==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/parser": "^7.24.7", + "@babel/plugin-transform-class-properties": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/preset-flow": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@babel/register": "^7.24.6", + "flow-parser": "0.*", + "graceful-fs": "^4.2.4", + "neo-async": "^2.5.0", + "picocolors": "^1.0.1", + "picomatch": "^4.0.2", + "recast": "^0.23.11", + "tmp": "^0.2.3", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "jscodeshift": "bin/jscodeshift.js" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "@babel/preset-env": "^7.1.6" + }, + "peerDependenciesMeta": { + "@babel/preset-env": { + "optional": true + } + } + }, + "node_modules/jscodeshift/node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6650,7 +7341,6 @@ "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "dev": true, "license": "MIT", "bin": { "json5": "lib/cli.js" @@ -6685,6 +7375,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -6733,12 +7432,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", - "license": "MIT" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -6772,7 +7465,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, "license": "ISC", "dependencies": { "yallist": "^3.0.2" @@ -6787,6 +7479,37 @@ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "license": "MIT", + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7493,6 +8216,12 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "license": "MIT" + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -7526,7 +8255,6 @@ "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7726,6 +8454,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7852,6 +8589,79 @@ "node": ">= 6" } }, + "node_modules/pkg-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", + "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "license": "MIT", + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8101,42 +8911,56 @@ "license": "MIT" }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } }, "node_modules/react-day-picker": { - "version": "8.10.2", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.2.tgz", - "integrity": "sha512-LK68OTbHB3oJNhl9cA0qVizzp3o26w61YSjAFkYi67N86iro32wx86kSNeFU/hq+gI8m1yzWhnomMLfZ041RzQ==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", + "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", "license": "MIT", + "dependencies": { + "@date-fns/tz": "^1.4.1", + "@tabby_ai/hijri-converter": "1.0.5", + "date-fns": "^4.1.0", + "date-fns-jalali": "4.1.0-0" + }, + "engines": { + "node": ">=18" + }, "funding": { "type": "individual", "url": "https://github.com/sponsors/gpbl" }, "peerDependencies": { - "date-fns": "^2.28.0 || ^3.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": ">=16.8.0" + } + }, + "node_modules/react-day-picker/node_modules/date-fns": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.4.0.tgz", + "integrity": "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.8" } }, "node_modules/react-hook-form": { @@ -8205,6 +9029,29 @@ "react": ">=18" } }, + "node_modules/react-redux": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.3.0.tgz", + "integrity": "sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", @@ -8304,21 +9151,6 @@ "react-dom": ">=16.8" } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -8378,45 +9210,76 @@ "node": ">=8.10.0" } }, - "node_modules/recharts": { - "version": "2.15.4", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.4.tgz", - "integrity": "sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==", - "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", "license": "MIT", "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.4", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" }, "engines": { - "node": ">=14" + "node": ">= 4" + } + }, + "node_modules/recast/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/recharts": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.10.1.tgz", + "integrity": "sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==", + "license": "MIT", + "workspaces": [ + "www" + ], + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^11.1.8", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" }, "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", - "license": "MIT", - "dependencies": { - "decimal.js-light": "^2.4.1" - } - }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", "license": "MIT" }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -8668,19 +9531,15 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8735,6 +9594,18 @@ "node": ">= 0.4" } }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8834,6 +9705,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/sonner": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", @@ -8862,6 +9745,25 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/space-separated-tokens": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", @@ -9030,6 +9932,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/styleq": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/styleq/-/styleq-0.2.1.tgz", + "integrity": "sha512-L0TR0NQb+X4/ktDEKmjWyp27gla+LUYi/by5k5SjKXf6/pvZP7wbwEB5J+tqxdFVPgzbsuz+d4RTScO/QZquBw==", + "license": "MIT" + }, "node_modules/stylis": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", @@ -9241,6 +10149,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tmp": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.7.tgz", + "integrity": "sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -9507,7 +10424,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.0.tgz", "integrity": "sha512-x/M6q3w4Ybp91CNaS4S69UnliqR3BzRpOT6LWbksjth0S/+jhfaPJsWjt/TewpT8j9eLIojUf5jr29WextHroA==", - "dev": true, "funding": [ { "type": "opencollective", @@ -9653,9 +10569,9 @@ } }, "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", @@ -9895,11 +10811,23 @@ "node": ">=0.10.0" } }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, "license": "ISC" }, "node_modules/yaml": { diff --git a/package.json b/package.json index 62feab7..8d7bf15 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,9 @@ "preview": "vite preview" }, "dependencies": { + "@astryxdesign/cli": "^0.3.0", + "@astryxdesign/core": "^0.3.0", + "@astryxdesign/theme-neutral": "^0.3.0", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.1", "@hookform/resolvers": "^4.1.2", @@ -44,6 +47,7 @@ "@radix-ui/react-toggle": "^1.1.2", "@radix-ui/react-toggle-group": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.8", + "@stylexjs/stylex": "^0.19.0", "@tanstack/react-query": "^5.84.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -56,15 +60,15 @@ "lucide-react": "^0.475.0", "moment": "^2.30.1", "next-themes": "^0.4.4", - "react": "^18.2.0", - "react-day-picker": "^8.10.1", - "react-dom": "^18.2.0", + "react": "^19.2.8", + "react-day-picker": "^9.14.0", + "react-dom": "^19.2.8", "react-hook-form": "^7.54.2", "react-hot-toast": "^2.6.0", "react-markdown": "^9.0.1", "react-resizable-panels": "^2.1.7", "react-router-dom": "^6.26.0", - "recharts": "^2.15.4", + "recharts": "^3.10.1", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", @@ -74,8 +78,8 @@ "devDependencies": { "@eslint/js": "^9.19.0", "@types/node": "^22.13.5", - "@types/react": "^18.2.66", - "@types/react-dom": "^18.2.22", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.4", "@vitejs/plugin-react": "^4.3.4", "autoprefixer": "^10.4.20", "eslint": "^9.19.0", diff --git a/src/App.jsx b/src/App.jsx index aaad173..b298517 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,5 @@ import { Toaster } from "@/components/ui/toaster" +import { Toaster as HotToaster } from 'react-hot-toast'; import { QueryClientProvider } from '@tanstack/react-query' import { queryClientInstance } from '@/lib/query-client' import { Navigate, BrowserRouter as Router, Route, Routes, useParams } from 'react-router-dom'; @@ -48,6 +49,12 @@ const LegacyCourseRedirect = () => { return ; }; +/** `/positions/:id` → the same position inside the Admin shell, id preserved. */ +const LegacyPositionRedirect = () => { + const { id } = useParams(); + return ; +}; + const AuthenticatedApp = () => { const { isLoadingAuth, isLoadingPublicSettings } = useAuth(); @@ -96,6 +103,10 @@ const AuthenticatedApp = () => { {/* Redirect top-level legacy aliases to the unified global shell */} } /> } /> + {/* `/positions` only matches the exact path, so the sub-routes need + their own aliases or an inbound link lands on the 404 page. */} + } /> + } /> } /> } /> } /> @@ -123,6 +134,11 @@ function App() { + {/* The design system's `toast` helper wraps react-hot-toast, which needs + its own renderer mounted. Without this every toast in the app fired + into nothing — confirmations, errors and the "not in the demo" + notices all silently did nothing. */} + ) diff --git a/src/api/base44Client.js b/src/api/base44Client.js index dca27e7..39d5e35 100644 --- a/src/api/base44Client.js +++ b/src/api/base44Client.js @@ -55,6 +55,24 @@ const auth = { return { ...currentUser }; }, + /** + * Preferences, read synchronously. + * + * `me()` is async because the real client fetches, but the panel provider has + * to decide whether Owliver starts open during its first render — one tick + * later is a visible flash of the wrong workspace. The session user is already + * hydrated from localStorage at module load, so this is a plain read of the + * same record `me()` returns, not a second copy of the state. + */ + preferences() { + return { ...DEMO_USER.preferences, ...(currentUser.preferences || {}) }; + }, + + /** Merges into the stored preferences and persists with the rest of the user. */ + async updatePreferences(patch) { + return auth.updateMe({ preferences: { ...auth.preferences(), ...patch } }); + }, + isAuthenticated() { return true; }, diff --git a/src/api/seed.js b/src/api/seed.js index 693f2d1..95c0231 100644 --- a/src/api/seed.js +++ b/src/api/seed.js @@ -1399,6 +1399,13 @@ export const DEMO_USER = { role: 'admin', account_type: 'employer', created_date: iso('2026-06-01'), + /* Product preferences travel with the account rather than in a store of their + own, so there is one record to persist and one thing to read. */ + preferences: { + owliverDefault: true, + compactDensity: false, + emailDigest: true, + }, }; export const seedData = { diff --git a/src/components/ai-assistant/AssistantPanel.jsx b/src/components/ai-assistant/AssistantPanel.jsx index 79c56a8..defece7 100644 --- a/src/components/ai-assistant/AssistantPanel.jsx +++ b/src/components/ai-assistant/AssistantPanel.jsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import { PanelLeftOpen, Sparkles } from 'lucide-react'; +import { PanelLeftOpen } from 'lucide-react'; import { cn } from '@/lib/utils'; import KrowAssistant from './KrowAssistant'; import { useAssistantPanel } from './AssistantPanelContext'; diff --git a/src/components/ai-assistant/AssistantPanelContext.jsx b/src/components/ai-assistant/AssistantPanelContext.jsx index 332d66a..0eb41cf 100644 --- a/src/components/ai-assistant/AssistantPanelContext.jsx +++ b/src/components/ai-assistant/AssistantPanelContext.jsx @@ -1,4 +1,5 @@ import * as React from 'react'; +import { base44 } from '@/api/base44Client'; import { resolveAssistantContext } from './placement'; /** @@ -56,7 +57,15 @@ function writeFlag(key, value) { } export function AssistantPanelProvider({ role, pathname, children }) { - const [open, setOpen] = React.useState(() => readFlag(OPEN_KEY, true)); + /* Two different things, deliberately kept apart: the account preference is + whether Owliver *should* open by default, and the session flag is whether + this tab currently has it collapsed. The preference is the fallback, so a + panel collapsed for the afternoon does not rewrite the account setting, and + an account with Owliver off does not get the panel pushed back at it on + every new tab. */ + const [open, setOpen] = React.useState( + () => readFlag(OPEN_KEY, base44.auth.preferences().owliverDefault !== false) + ); const [expanded, setExpanded] = React.useState(() => readFlag(EXPANDED_KEY)); const [width, setWidth] = React.useState(() => { try { diff --git a/src/components/ai-assistant/KrowAssistant.jsx b/src/components/ai-assistant/KrowAssistant.jsx index be0586d..bbc5201 100644 --- a/src/components/ai-assistant/KrowAssistant.jsx +++ b/src/components/ai-assistant/KrowAssistant.jsx @@ -1,9 +1,15 @@ import * as React from 'react'; +import { useNavigate } from 'react-router-dom'; import { Maximize2, Minimize2, PanelRightClose, RotateCcw, Sparkles } from 'lucide-react'; import { cn } from '@/lib/utils'; import { Surface } from '@/components/ds/Surface'; import { IconButton } from '@/components/ds/IconButton'; import { Alert } from '@/components/ds/Alert'; +import { useCreateJobPosting, usePreferences, useRoleCategories } from '@/lib/krowHooks'; +import { ROLE_CATEGORIES } from '@/lib/roleCategories'; +import { AddSkillDialog } from '@/components/skills/AddSkillDialog'; +import { runAction } from '@/lib/skills/actions'; +import { skillsForContext } from '@/lib/skills/registry'; import { useAssistantFacts, useConversation, useCurrentUserName } from './useAssistant'; import { buildIntro, buildPrompts } from './dynamic'; import { Message, ThinkingIndicator, TurnDivider } from './AssistantMessage'; @@ -42,9 +48,83 @@ export default function KrowAssistant({ }) { const facts = useAssistantFacts(); const userName = useCurrentUserName(); + const navigate = useNavigate(); + + /* The app's own router, not a location assignment: a full page load would + discard the thread and the panel state along with it. */ + const goToPage = React.useCallback( + (destination) => navigate(destination.route), + [navigate] + ); + + /* Skills available here, and the roles they can act on — both read from what + the app already has rather than from anything written into this component. + A page whose skills change needs no edit here. */ + const preferences = usePreferences(); + const disabledSkills = React.useMemo( + () => preferences.disabledSkills || [], + [preferences.disabledSkills] + ); + const { data: customCategories = [] } = useRoleCategories(); + const roles = React.useMemo( + () => [...new Set([...ROLE_CATEGORIES, ...customCategories.map((c) => c.name)])], + [customCategories] + ); + /* The categories Forge skills are actually filed under, so "create a kitchen + safety training" fills in the category this deployment uses rather than one + written into a skill file. */ + const skillCategories = React.useMemo( + () => [...new Set((facts.forge?.library || []).map((c) => c.category).filter(Boolean))], + [facts.forge] + ); + const customSkills = React.useMemo( + () => preferences.customSkills || [], + [preferences.customSkills] + ); + const skills = React.useMemo( + () => skillsForContext(context.id, disabledSkills, customSkills), + [context.id, disabledSkills, customSkills] + ); + const [addSkillOpen, setAddSkillOpen] = React.useState(false); + + /** + * A skill asked for something to happen. The names come from the skill file; + * `runAction` decides what they mean and refuses anything the file did not + * declare. + */ + const performAction = React.useCallback((action, skill) => { + const result = runAction(action.name, { ...action.payload, skill }); + if (result?.type === 'navigate' && result.route) { + navigate(result.route, result.state ? { state: result.state } : undefined); + } + }, [navigate]); + + /** + * Write the position the conversation collected. + * + * The same mutation the Create Position form calls, so there is one create + * path and the list behind the panel refreshes the way it always did. The + * skill still has to declare `create_position` — `runAction` returns nothing + * for a skill whose file does not, and then nothing is written. + */ + const createJob = useCreateJobPosting(); + const createPosition = React.useCallback(async (draft, skill) => { + const result = runAction('create_position', { draft, skill }); + if (result?.type !== 'create_position') return null; + return createJob.mutateAsync(result.data); + }, [createJob]); + const { messages, pending, error, busy, send, stop, reset } = useConversation({ contextId: context.id, facts, + onNavigate: goToPage, + onAction: performAction, + onCreatePosition: createPosition, + disabledSkills, + customSkills, + roles, + skillCategories, + courses: facts.forge?.library || [], }); const [input, setInput] = React.useState(''); @@ -57,7 +137,17 @@ export default function KrowAssistant({ () => buildIntro(context.id, facts, userName), [context.id, facts, userName] ); - const prompts = React.useMemo(() => buildPrompts(context.id, facts), [context.id, facts]); + /* Suggestions are the page's own, with any skill that offers one on the front. + The last answer can also propose follow-ups — the role list after "create a + position" — which replace the standing set until the next turn. */ + const followUp = messages[messages.length - 1]?.followUp; + const prompts = React.useMemo(() => { + if (followUp?.length) return followUp; + const skillPrompts = skills + .filter((s) => s.prompt) + .map((s) => ({ label: s.prompt, prompt: s.prompt })); + return [...skillPrompts, ...buildPrompts(context.id, facts)]; + }, [followUp, skills, context.id, facts]); /* Follow the newest content. Direct `scrollTop` rather than smooth scrolling: at streaming frequency a smooth scroll never catches up and the thread @@ -67,19 +157,36 @@ export default function KrowAssistant({ if (el) el.scrollTop = el.scrollHeight; }, [messages, pending]); - const submit = React.useCallback((text) => { - const question = String(text).trim(); - if (!question || busy) return; - send({ question }); + /** + * The one entry point for every message, whatever raised it. + * + * A chip carries a capability — it *is* one of this page's answers, so it + * bypasses intent routing. A typed question carries none and is routed. Both + * land in the same `send`, so there is a single pipeline to reason about. + */ + const handleMessage = React.useCallback((question, capability = null) => { + const text = String(question).trim(); + if (!text || busy) return; setInput(''); + send({ question: text, capability }); }, [busy, send]); - /* A chip is already a complete question, so it runs on click. */ + const submit = React.useCallback((text) => handleMessage(text), [handleMessage]); + + /** + * A chip is already a complete question, so it runs on click. + * + * A chip may instead carry a destination — "View position", offered after + * Owliver creates one. That is the user choosing to go somewhere after the + * work is finished, which is the only navigation any of this does. + */ const runPrompt = React.useCallback((prompt) => { - if (busy) return; - setInput(''); - send({ question: prompt.prompt, capability: prompt.capability }); - }, [busy, send]); + if (prompt.route) { + navigate(prompt.route); + return; + } + handleMessage(prompt.prompt, prompt.capability); + }, [handleMessage, navigate]); const composer = ( + {/* Opens the same dialog Settings does; there is one registry behind + both, so a skill added here is the skill added there. */} + setAddSkillOpen(true)} + /> {messages.length > 0 && ( )} @@ -179,17 +295,23 @@ export default function KrowAssistant({ )} + + {/* ── Composer: fixed to the bottom in both states ───────────────── */}
{/* All suggestions on the landing screen, where they teach what can be asked. Capped once a thread exists, because from then on the vertical space belongs to the conversation. Expanded fits more per line, so it - can afford one more. */} + can afford one more. + + Follow-ups are never capped: when Owliver has asked a question, its + chips *are* the answers, and hiding three of the six roles would make + the flow look broken. */} {!busy && ( )} diff --git a/src/components/ai-assistant/capabilities/admin.js b/src/components/ai-assistant/capabilities/admin.js index 7c4083e..63024bc 100644 --- a/src/components/ai-assistant/capabilities/admin.js +++ b/src/components/ai-assistant/capabilities/admin.js @@ -8,7 +8,11 @@ import { text, timeline, } from '../blocks'; import { pct, plural, verb } from '../insights'; +/* The proof-method wording is the Forge page's own, so a skill is never named + one thing in the library and another in the panel beside it. */ +import { TYPE_LABEL } from '@/components/forge/challengeMeta'; import { toFICO } from '@/lib/talentHome'; +import { PERMISSIONS, PROFILE_ACTIONS } from '@/pages/admin/permissions'; /** * Admin capabilities — Control Center, Candidates Analysis, Activity. @@ -1357,161 +1361,162 @@ export const ACTIVITY_CAPABILITIES = [ /* ── KROW Forge ─────────────────────────────────────────────────────────── */ /** - * Forge capabilities — workforce skill verification. + * Forge capabilities — managing 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. + * The Forge questions an administrator asks are not a worker's. A worker asks + * what to prove next; the person who owns the library asks what is in it, what + * is live, what Owliver is checking, and whether any of it is reaching the + * workforce. Every figure comes from `f.forge`, derived from the same Course + * records and worker profiles the page renders from. */ -const forgeProgress = (f) => { +const forgeLibrary = (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.`); + if (!g.library.length) { + return answer('The library is empty. A skill defines what the workforce learns, how they prove it, and what Owliver checks — start one from Add Skill Training.'); } 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.`, + heading('Skill library', `${plural(g.library.length, 'skill')} defined`), + kpis([ + { label: 'Active skills', value: g.published.length, sub: `of ${g.library.length} in the library` }, + { label: 'Training modules', value: g.withTraining.length, sub: 'carrying material' }, + { label: 'Challenges', value: g.withChallenge.length, sub: 'evidence-based' }, + { label: 'Verified skills', value: g.workforceVerified, sub: g.workforceVerified ? 'earned by the workforce' : 'none earned yet' }, + ]), + g.byCategory.length ? heading('By category') : null, + meters(g.byCategory.map((c) => ({ + label: c.category, + value: c.count, + max: g.library.length, }))), - note('Requirements are measured against the profile record, so they update as shifts and reliability are logged.') + g.drafts.length + ? note(`${plural(g.drafts.length, 'skill')} ${verb(g.drafts.length, 'is', 'are')} still in draft and not visible to the workforce.`) + : null ); }; -const forgeReadyNow = (f) => { +const forgePublished = (f) => { const g = f.forge; - if (!g.ready.length) { + if (!g.published.length) { + return answer(`Nothing is published. ${g.drafts.length ? `${plural(g.drafts.length, 'draft')} ${verb(g.drafts.length, 'is', 'are')} waiting on a decision.` : 'The library has no skills yet.'}`); + } + + return doc( + heading(`${plural(g.published.length, 'skill')} published`, 'live to the workforce'), + table( + [ + { key: 'skill', label: 'Skill' }, + { key: 'category', label: 'Category' }, + { key: 'proof', label: 'Proof' }, + { key: 'minutes', label: 'Min', align: 'right' }, + ], + g.published.map((c) => ({ + skill: c.title, + category: c.category || '—', + proof: TYPE_LABEL[c.challenge?.type] || 'Not set', + minutes: c.estimated_minutes || '—', + })) + ), + g.drafts.length + ? insights([{ + tone: 'warning', + title: `${plural(g.drafts.length, 'skill')} in draft`, + body: `${g.drafts.map((c) => c.title).join(', ')}. A draft is invisible to the workforce until it is published.`, + }]) + : note('Every skill in the library is published.') + ); +}; + +const forgeEvaluation = (f) => { + const g = f.forge; + const scored = g.library.filter((c) => c.challenge?.rubric?.length); + + if (!scored.length) { + return answer('No skill has evaluation criteria yet. Owliver scores a submission against the criteria set on the skill, so a skill without them cannot verify anything.'); + } + + return doc( + heading('What Owliver evaluates', `across ${plural(scored.length, 'skill')}`), + table( + [ + { key: 'skill', label: 'Skill' }, + { key: 'criteria', label: 'Criteria' }, + { key: 'pass', label: 'Passes at', align: 'right' }, + ], + scored.map((c) => ({ + skill: c.title, + criteria: c.challenge.rubric.map((r) => r.criterion).join(', '), + pass: c.pass_score || '—', + })) + ), + g.library.length > scored.length + ? insights([{ + tone: 'warning', + title: 'Skills with nothing to score against', + body: `${g.library.filter((c) => !c.challenge?.rubric?.length).map((c) => c.title).join(', ')}. Owliver needs criteria to return a verdict.`, + }]) + : note('Every skill in the library tells Owliver what to check.') + ); +}; + +const forgeWorkforce = (f) => { + const g = f.forge; + const touched = g.usage.filter((u) => u.completed > 0 || u.verified > 0); + + if (!touched.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 + text('No skill in the library has been completed by anyone yet.'), + note('Completion and verification are read from worker profiles, so figures appear here as the workforce works through the library.') ); } 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')), + heading('Workforce usage', `${plural(touched.length, 'skill')} in use`), table( [ { key: 'skill', label: 'Skill' }, - { key: 'state', label: 'State' }, - { key: 'xp', label: 'XP', align: 'right' }, + { key: 'completed', label: 'Completed', align: 'right' }, + { key: 'verified', label: 'Verified', align: 'right' }, ], - skills + touched + .sort((a, b) => b.verified - a.verified || b.completed - a.completed) + .map((u) => ({ + skill: u.course.title, + completed: u.completed, + verified: u.verified, + })) ), - 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.' - ) + g.library.length > touched.length + ? note(`${plural(g.library.length - touched.length, 'skill')} ${verb(g.library.length - touched.length, 'has', 'have')} no completions on record.`) + : null + ); +}; + +const forgeGaps = (f) => { + const g = f.forge; + + const gaps = [ + ...g.drafts.map((c) => ({ skill: c.title, gap: 'Unpublished draft' })), + ...g.library.filter((c) => !c.challenge?.type).map((c) => ({ skill: c.title, gap: 'No proof method' })), + ...g.library.filter((c) => !c.challenge?.rubric?.length).map((c) => ({ skill: c.title, gap: 'No Owliver criteria' })), + ...g.library.filter((c) => !c.training_outline?.length && !c.quiz?.length).map((c) => ({ skill: c.title, gap: 'No training material' })), + ]; + + if (!gaps.length) { + return answer(`All ${plural(g.library.length, 'skill')} carry training, a proof method and evaluation criteria, and every one is published.`); + } + + return doc( + heading(`${plural(gaps.length, 'gap')} to close`, 'before these verify anything'), + table( + [ + { key: 'skill', label: 'Skill' }, + { key: 'gap', label: 'Missing' }, + ], + gaps + ), + note('A skill needs training, a way to submit evidence, and criteria for Owliver to score against before it can verify a workforce skill.') ); }; @@ -1763,12 +1768,12 @@ const hiringRecentHires = (f) => { 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); + if (has(q, 'publish', 'live', 'draft', 'unpublish', 'archived', 'status')) return forgePublished(f); + if (has(q, 'owliver', 'evaluat', 'criteri', 'rubric', 'grade', 'score against', 'pass')) return forgeEvaluation(f); + if (has(q, 'gap', 'missing', 'incomplete', 'unfinished', 'needs', 'ready to publish')) return forgeGaps(f); + if (has(q, 'workforce', 'usage', 'completed', 'verified', 'earned', 'badge', 'who has', 'uptake')) return forgeWorkforce(f); + if (has(q, 'skill', 'library', 'challenge', 'course', 'training', 'category', 'summar', 'overview', 'how many', 'what do we have')) return forgeLibrary(f); + return forgeLibrary(f); }; export const respondTalentPool = (question, f) => { @@ -1793,11 +1798,11 @@ export const respondHiredHistory = (question, 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 }, + { id: 'forge-library', label: 'Skill Library', icon: Gauge, run: forgeLibrary }, + { id: 'forge-published', label: 'Published Skills', icon: Target, run: forgePublished }, + { id: 'forge-evaluation', label: 'Owliver Evaluation', icon: ShieldCheck, run: forgeEvaluation }, + { id: 'forge-workforce', label: 'Workforce Usage', icon: UserCheck, run: forgeWorkforce }, + { id: 'forge-gaps', label: 'Gaps To Close', icon: Lock, run: forgeGaps }, ]; export const TALENT_POOL_CAPABILITIES = [ @@ -1813,3 +1818,120 @@ export const HIRED_HISTORY_CAPABILITIES = [ { id: 'hiring-patterns', label: 'What Stands Out', icon: Layers, run: hiringPatterns }, { id: 'hiring-recent', label: 'Recent Hires', icon: ScrollText, run: hiringRecentHires }, ]; + +/* ── Profile ──────────────────────────────────────────────────────────────── + * + * The account page answers questions about the operator rather than the + * workforce: what this account may do, how it is secured, what it has been + * doing, and which switches are on. Everything below reads the same constants + * the page renders, so the panel can never describe a scope or a control that + * is not on screen. + */ + +const profilePermissions = () => doc( + heading('Your permissions', `${PERMISSIONS.length} scopes on this account`), + table( + [{ key: 'scope', label: 'Scope' }, { key: 'level', label: 'Level' }, { key: 'detail', label: 'Covers' }], + PERMISSIONS.map((p) => ({ + scope: p.scope, + level: { + value: p.level, + tone: p.level === 'Full access' ? 'success' : p.level === 'Read only' ? 'info' : 'muted', + }, + detail: p.detail, + })) + ), + note('Scopes are set by the account owner. Billing stays restricted to owners.') +); + +const profileIdentity = (f) => { + const user = f.user || {}; + return doc( + heading('This account'), + table( + [{ key: 'field', label: 'Field' }, { key: 'value', label: 'Value' }], + [ + { field: 'Name', value: user.full_name || '—' }, + { field: 'Email', value: user.email || '—' }, + { field: 'Role', value: 'Platform Administrator' }, + { field: 'Department', value: 'Workforce Operations' }, + { field: 'Account status', value: { value: 'Active · verified', tone: 'success' } }, + { + field: 'Member since', + value: user.created_date + ? new Date(user.created_date).toLocaleDateString(undefined, { month: 'long', year: 'numeric' }) + : '—', + }, + ] + ), + text('Use **Edit profile** at the top of the page to change your display name. Email and role are set by the account owner.') + ); +}; + +const profileSecurity = () => doc( + heading('Security on this account'), + status([ + { label: 'Two-factor authentication', ok: true, value: 'On', note: 'Required for privileged actions.' }, + { label: 'Password age', ok: true, value: '42 days', note: 'Administrator passwords expire every 90 days.' }, + { label: 'Session', ok: true, value: 'Browser tab', note: 'Closing the tab signs this account out.' }, + ]), + text('**Change password** and **Manage sessions** sit in the Security card on the right.') +); + +const profilePreferences = (f) => { + const p = f.user?.preferences || {}; + const state = (on) => ({ value: on ? 'On' : 'Off', tone: on ? 'success' : 'muted' }); + return doc( + heading('Your preferences'), + table( + [{ key: 'setting', label: 'Setting' }, { key: 'state', label: 'State' }, { key: 'detail', label: 'Effect' }], + [ + { setting: 'Owliver workspace', state: state(p.owliverDefault !== false), detail: 'Opens this panel by default on supported pages' }, + { setting: 'Compact density', state: state(Boolean(p.compactDensity)), detail: 'Tighter row heights across Admin tables' }, + { setting: 'Email digest', state: state(p.emailDigest !== false), detail: 'A daily summary of what needs attention' }, + ] + ), + note('Preferences save to your account as soon as you flip a switch.') + ); +}; + +const profileActivity = (f) => { + const email = f.user?.email; + const mine = f.activity.filter((e) => e.user_email === email).slice(0, 6); + if (!mine.length) { + return doc(text('No activity is recorded against this account yet.')); + } + return doc( + heading('Your recent activity', `${plural(mine.length, 'event')} on this account`), + timeline(mine.map((e) => ({ + title: e.event_type.replace(/_/g, ' '), + description: e.details || undefined, + timestamp: new Date(e.created_date).toLocaleDateString(undefined, { month: 'short', day: 'numeric' }), + }))) + ); +}; + +const profileActions = () => doc( + heading('What you can do here'), + list(PROFILE_ACTIONS.map((a) => `**${a.label}** — ${a.detail}`)), + note('Everything above is on this page. Nothing here changes another operator’s account.') +); + +export const respondProfile = (question, f) => { + const q = question.toLowerCase(); + if (has(q, 'permission', 'scope', 'access', 'allowed', 'can i')) return profilePermissions(f); + if (has(q, 'password', 'two-factor', 'two factor', '2fa', 'security', 'session', 'sign out', 'log out')) return profileSecurity(f); + if (has(q, 'preference', 'setting', 'digest', 'density', 'workspace', 'owliver')) return profilePreferences(f); + if (has(q, 'activity', 'recent', 'history', 'what have i')) return profileActivity(f); + if (has(q, 'do here', 'change my', 'edit', 'update', 'how do i')) return profileActions(f); + return profileIdentity(f); +}; + +export const PROFILE_CAPABILITIES = [ + { id: 'profile-permissions', label: 'My Permissions', icon: ShieldCheck, run: profilePermissions }, + { id: 'profile-identity', label: 'Account Details', icon: Users, run: profileIdentity }, + { id: 'profile-security', label: 'Security', icon: Lock, run: profileSecurity }, + { id: 'profile-preferences', label: 'My Preferences', icon: Gauge, run: profilePreferences }, + { id: 'profile-activity', label: 'My Recent Activity', icon: ScrollText, run: profileActivity }, + { id: 'profile-actions', label: 'What I Can Do Here', icon: Lightbulb, run: profileActions }, +]; diff --git a/src/components/ai-assistant/contexts.js b/src/components/ai-assistant/contexts.js index 0645e75..9a773ec 100644 --- a/src/components/ai-assistant/contexts.js +++ b/src/components/ai-assistant/contexts.js @@ -5,10 +5,11 @@ import { import { ACTIVITY_CAPABILITIES, ADMIN_ANALYTICS_CAPABILITIES, ADMIN_CANDIDATE_CAPABILITIES, CANDIDATE_LIST_CAPABILITIES, CONTROL_CENTER_CAPABILITIES, FORGE_CAPABILITIES, - HIRED_HISTORY_CAPABILITIES, POSITIONS_CAPABILITIES, TALENT_POOL_CAPABILITIES, + HIRED_HISTORY_CAPABILITIES, POSITIONS_CAPABILITIES, PROFILE_CAPABILITIES, + TALENT_POOL_CAPABILITIES, respondActivity, respondAdminAnalytics, respondAdminCandidates, respondCandidateList, respondControlCenter, respondForge, respondHiredHistory, respondPositions, - respondTalentPool, + respondProfile, respondTalentPool, } from './capabilities/admin'; /** @@ -21,6 +22,11 @@ import { * * Krow AI serves Employer and Admin. The talent portal has Owliver, a separate * product with its own UI and workflow — nothing here targets it. + * + * `topics` is what this page's answers are actually about. A question matching + * none of them is not quietly answered with the page's default report — see + * routing.js. Contexts without `topics` keep answering everything, so declaring + * them is opt-in per page. */ export const ASSISTANT_CONTEXTS = { 'employer.overview': { @@ -44,12 +50,14 @@ export const ASSISTANT_CONTEXTS = { 'admin.controlCenter': { id: 'admin.controlCenter', page: 'Control Center', + topics: ['health', 'platform', 'attention', 'urgent', 'bottleneck', 'funnel', 'pipeline', 'conversion', 'recommend', 'should', 'summary', 'summarize', 'workforce', 'operation', 'velocity', 'speed', 'hiring', 'how many', 'unusual', 'anomal', 'risk', 'overview', 'status'], capabilities: CONTROL_CENTER_CAPABILITIES, respond: respondControlCenter, }, 'admin.positions': { id: 'admin.positions', page: 'Positions', + topics: ['position', 'role', 'posting', 'vacancy', 'fill', 'attention', 'priorit', 'bottleneck', 'funnel', 'waiting', 'review', 'screen', 'applicant', 'pipeline', 'strength', 'activity', 'hiring', 'how many', 'which'], capabilities: POSITIONS_CAPABILITIES, respond: respondPositions, }, @@ -59,12 +67,14 @@ export const ASSISTANT_CONTEXTS = { 'admin.candidatesList': { id: 'admin.candidatesList', page: 'Candidates', + topics: ['candidate', 'applicant', 'attention', 'waiting', 'interview', 'ready', 'score', 'unscored', 'risk', 'flag', 'strongest', 'best', 'top', 'compare', 'shortlist', 'pipeline', 'summar', 'how many', 'who'], capabilities: CANDIDATE_LIST_CAPABILITIES, respond: respondCandidateList, }, 'admin.candidates': { id: 'admin.candidates', page: 'Candidates Analysis', + topics: ['candidate', 'applicant', 'gap', 'missing', 'coverage', 'unscored', 'risk', 'flag', 'compare', 'top', 'best', 'strongest', 'shortlist', 'recommend', 'hire', 'who', 'pool', 'quality'], capabilities: ADMIN_CANDIDATE_CAPABILITIES, respond: respondAdminCandidates, }, @@ -73,33 +83,57 @@ export const ASSISTANT_CONTEXTS = { 'admin.analytics': { id: 'admin.analytics', page: 'Analytics', + topics: ['trend', 'over time', 'month', 'week', 'department', 'category', 'team', 'perform', 'bottleneck', 'funnel', 'conversion', 'position', 'role', 'score', 'attention', 'rate', 'average', 'breakdown', 'report', 'how many', 'compare'], 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. */ + /* Forge is an authoring console, so its topics are the author's: what the + library holds, what is live, what Owliver checks, and how the workforce is + doing with it. */ 'admin.forge': { id: 'admin.forge', page: 'KROW Forge', + topics: ['forge', 'skill', 'challenge', 'course', 'training', 'library', 'badge', + 'certification', 'publish', 'published', 'draft', 'archive', 'status', 'category', + 'owliver', 'evaluat', 'criteri', 'rubric', 'proof', 'evidence', 'verify', 'verification', + 'verified', 'workforce', 'gap', 'missing', 'what do we have'], capabilities: FORGE_CAPABILITIES, respond: respondForge, }, 'admin.talentPool': { id: 'admin.talentPool', page: 'Talent Pool', + topics: ['talent', 'pool', 'worker', 'profile', 'priorit', 'availab', 'verif', 'score', 'unscored', 'segment', 'supply', 'summar', 'how many', 'who', 'shortlist'], capabilities: TALENT_POOL_CAPABILITIES, respond: respondTalentPool, }, 'admin.hiredHistory': { id: 'admin.hiredHistory', page: 'Hired History', + topics: ['hire', 'hired', 'outcome', 'department', 'quality', 'recent', 'strongest', 'pattern', 'time to hire', 'retention', 'how many', 'who'], capabilities: HIRED_HISTORY_CAPABILITIES, respond: respondHiredHistory, }, + /* The account page. A form rather than a workflow, but the questions people + ask about it — what may I do, how is this secured, what did I change — are + answerable from what the page already shows. */ + 'admin.profile': { + id: 'admin.profile', + page: 'Profile', + topics: ['permission', 'scope', 'access', 'allowed', 'can i', 'account', 'role', + 'password', 'two-factor', 'two factor', '2fa', 'security', 'session', 'sign out', + 'preference', 'setting', 'digest', 'density', 'workspace', 'owliver', 'profile', + 'name', 'email', 'activity', 'recent', 'do here', 'how do i', 'edit', 'change my'], + capabilities: PROFILE_CAPABILITIES, + respond: respondProfile, + }, 'admin.activity': { id: 'admin.activity', page: 'Activity', + topics: ['activity', 'audit', 'event', 'log', 'security', 'breach', 'compliance', 'trace', 'unusual', 'anomal', 'suspicious', 'user', 'who', 'account', 'busiest'], capabilities: ACTIVITY_CAPABILITIES, respond: respondActivity, }, diff --git a/src/components/ai-assistant/dynamic.js b/src/components/ai-assistant/dynamic.js index 138e3bc..04da9bf 100644 --- a/src/components/ai-assistant/dynamic.js +++ b/src/components/ai-assistant/dynamic.js @@ -1,4 +1,5 @@ import { plural, verb } from './insights'; +import { PERMISSIONS } from '@/pages/admin/permissions'; /** * Dynamic greetings and suggested prompts. @@ -133,16 +134,20 @@ const GREETINGS = { return `${plural(f.hired.length, 'hire')} from ${plural(f.total, 'applicant')} — ${f.hireRate}% conversion.`; }, + /* The line the panel opens with: whatever most needs the author's attention, + which on an authoring console is an unfinished skill before it is a total. */ '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.library.length) return 'The library is empty — no skill is defined yet.'; + + const unscored = g.library.filter((c) => !c.challenge?.rubric?.length).length; + if (unscored) { + return `${plural(unscored, 'skill')} ${verb(unscored, 'has', 'have')} no Owliver criteria, so ${verb(unscored, 'it', 'they')} cannot verify anything yet.`; } - if (!g.verified.length) { - return `Nothing verified yet — ${plural(g.ready.length, 'challenge')} ${verb(g.ready.length, 'is', 'are')} ready to start.`; + if (g.drafts.length) { + return `${plural(g.drafts.length, 'skill')} ${verb(g.drafts.length, 'is', 'are')} in draft and not visible to the workforce.`; } - return `${plural(g.verified.length, 'skill')} verified, ${plural(g.ready.length, 'challenge')} ready to start.`; + return `${plural(g.published.length, 'skill')} published, ${plural(g.workforceVerified, 'verification')} earned.`; }, 'admin.talentPool': (f) => { @@ -165,6 +170,15 @@ const GREETINGS = { return `${plural(h.hires.length, 'hire')} placed, averaging ${h.avgQuality} at hire.`; }, + 'admin.profile': (f) => { + const mine = f.user + ? f.activity.filter((e) => e.user_email === f.user.email) + : []; + const full = PERMISSIONS.filter((p) => p.level === 'Full access').length; + if (!mine.length) return `${plural(full, 'scope')} at full access on this account.`; + return `${plural(full, 'scope')} at full access, ${plural(mine.length, 'event')} logged against this account.`; + }, + 'admin.activity': (f) => { if (!f.activity.length) return 'No activity logged yet.'; const patterns = f.activitySignals.flags.length; @@ -203,6 +217,7 @@ const TITLES = { '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}` : ''}`, + 'admin.profile': (f, name) => `Good ${timeOfDay(f.today)}${name ? `, ${name}` : ''}`, }; /** Descriptions state what this page's assistant can actually do with the data. */ @@ -223,11 +238,13 @@ const DESCRIPTIONS = { `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.`, + `Reading ${plural(f.forge.library.length, 'skill')} — training, proof, what Owliver evaluates and how the workforce is doing with them.`, '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.`, + 'admin.profile': () => + 'Reading this account — permissions, security, preferences and your own activity.', }; /** @@ -284,9 +301,9 @@ const PLACEHOLDERS = { '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…', + 'Ask Owliver to create a skill training…', + 'Ask Owliver what the library covers…', + 'Ask Owliver what a skill verifies…', ], 'admin.talentPool': () => [ 'Ask Owliver who to prioritize…', @@ -298,6 +315,11 @@ const PLACEHOLDERS = { 'Ask Owliver which roles hired best…', 'Ask Owliver what stands out…', ], + 'admin.profile': () => [ + 'Ask Owliver about your permissions…', + 'Ask Owliver what you can change here…', + 'Ask Owliver about your security settings…', + ], }; /* ── Suggested prompts ──────────────────────────────────────────────────── */ @@ -426,18 +448,22 @@ const PROMPTS = { rather than offering the same questions regardless of state. */ 'admin.forge': (f) => { const g = f.forge; + const unscored = g.library.filter((c) => !c.challenge?.rubric?.length).length; 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' }, + /* The chip that creates something goes first: on an authoring console the + most likely next action is authoring. It carries no capability, so it + routes through the skill registry rather than the page responder. */ + { label: 'Create a skill training', prompt: 'Create a skill training' }, + g.drafts.length + ? { label: `${g.drafts.length} in draft`, prompt: 'Which skills are published?', capability: 'forge-published' } + : { label: `${g.published.length} published`, prompt: 'Which skills are published?', capability: 'forge-published' }, + unscored + ? { label: `${unscored} without Owliver criteria`, prompt: 'What gaps are left in the library?', capability: 'forge-gaps' } + : { label: 'What does Owliver evaluate?', prompt: 'What does Owliver evaluate?', capability: 'forge-evaluation' }, + { label: 'What skills do we currently have?', prompt: 'What skills do we currently have?', capability: 'forge-library' }, + g.workforceVerified + ? { label: `${g.workforceVerified} skills verified`, prompt: 'How is the workforce doing with these skills?', capability: 'forge-workforce' } + : { label: 'How is the workforce using these?', prompt: 'How is the workforce doing with these skills?', capability: 'forge-workforce' }, ]; }, @@ -473,6 +499,22 @@ const PROMPTS = { ]; }, + 'admin.profile': (f) => { + const mine = f.user ? f.activity.filter((e) => e.user_email === f.user.email) : []; + const restricted = PERMISSIONS.filter((p) => p.level !== 'Full access').length; + return [ + { label: 'What are my permissions?', prompt: 'What are my permissions?', capability: 'profile-permissions' }, + restricted + ? { label: `${restricted} scopes are restricted`, prompt: 'Which scopes am I restricted on?', capability: 'profile-permissions' } + : { label: 'What can I do here?', prompt: 'What can I do on this page?', capability: 'profile-actions' }, + { label: 'How is my account secured?', prompt: 'How is my account secured?', capability: 'profile-security' }, + { label: 'What are my preferences set to?', prompt: 'What are my preferences set to?', capability: 'profile-preferences' }, + mine.length + ? { label: `My last ${mine.length} actions`, prompt: 'What is my recent activity?', capability: 'profile-activity' } + : { label: 'What can I change here?', prompt: 'What can I change on this page?', capability: 'profile-actions' }, + ]; + }, + '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 ebd9651..5cd06d4 100644 --- a/src/components/ai-assistant/insights.js +++ b/src/components/ai-assistant/insights.js @@ -41,6 +41,7 @@ export function buildFacts({ activity = [], courses = [], profile = null, + user = null, today = new Date(), }) { const scored = applications.filter((a) => a.ai_score > 0); @@ -207,6 +208,37 @@ export function buildFacts({ level, count: forgeCourses.filter((c) => c.difficulty === level).length, })).filter((d) => d.count), + + /* ── The same library, read by the administrator who owns it ────────── + Everything above answers "what can I prove"; everything below answers + "what have we built, and how is the workforce doing with it". Both are + derived from the same records, so the panel cannot disagree with the page + whichever question is asked. Publication state is read with the same + mapping the Forge page uses: `active` is Published. */ + library: courses, + published: courses.filter((c) => c.status === 'active' || c.status === 'published'), + drafts: courses.filter((c) => c.status === 'draft'), + archived: courses.filter((c) => c.status === 'archived' || c.status === 'inactive'), + withChallenge: courses.filter((c) => c.challenge?.type), + withTraining: courses.filter((c) => c.training_outline?.length || c.quiz?.length), + byCategory: [...courses.reduce((map, c) => { + const key = c.category || 'Uncategorised'; + map.set(key, (map.get(key) || 0) + 1); + return map; + }, new Map())] + .map(([category, count]) => ({ category, count })) + .sort((a, b) => b.count - a.count), + /* Verifications the workforce actually holds, counted off worker profiles + rather than off the library — a badge is something a person has. */ + workforceVerified: profiles.reduce((sum, p) => { + const ids = new Set(courses.map((c) => c.id)); + return sum + (p.earned_badges || []).filter((b) => ids.has(b.course_id)).length; + }, 0), + usage: courses.map((c) => ({ + course: c, + completed: profiles.filter((p) => (p.completed_courses || []).some((x) => x.course_id === c.id)).length, + verified: profiles.filter((p) => (p.earned_badges || []).some((b) => b.course_id === c.id)).length, + })), }; /* ── Talent pool ───────────────────────────────────────────────────────── @@ -281,6 +313,10 @@ export function buildFacts({ return { today, + /* The signed-in account — identity and preferences, for the pages that are + about the operator rather than the workforce. */ + user, + /* Applications */ applications, total: applications.length, diff --git a/src/components/ai-assistant/placement.js b/src/components/ai-assistant/placement.js index 0e62913..81c22d2 100644 --- a/src/components/ai-assistant/placement.js +++ b/src/components/ai-assistant/placement.js @@ -22,6 +22,7 @@ const PLACEMENT = { '/admin/university': 'admin.forge', '/admin/talent-pool': 'admin.talentPool', '/admin/hired': 'admin.hiredHistory', + '/admin/profile': 'admin.profile', }, }; @@ -29,9 +30,9 @@ const PLACEMENT = { * Pages that must never carry the assistant, listed explicitly so the intent is * documented rather than implied by omission. * - * 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. + * Admin: login only. Every other surface — including Profile, where the + * questions are about the account rather than the workforce — 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. @@ -42,12 +43,19 @@ export const EXCLUDED_ROUTES = [ '/hired', '/talent-pool', '/analytics', '/profile', '/apply', '/university', '/university/:id', '/me', '/identity', '/employee', '/owliver', '/design-system', - // Admin pages that are a form rather than a workflow. - '/admin/profile', '/admin/login', + // Sign-in has no page data to reason about. + '/admin/login', // The full candidate profile is one person's record, not a page-level question. '/admin/candidates/:id', ]; +/** + * The Admin route → context table, exported so the skill registry can derive + * page keys from it. One table, so a route added here is addressable by a skill + * without a second list to keep in step. + */ +export const PLACEMENT_ROUTES = PLACEMENT.admin; + /** Returns the context for a role and path, or `null` to render no assistant. */ export function resolveAssistantContext(role, pathname) { const id = PLACEMENT[role]?.[pathname]; diff --git a/src/components/ai-assistant/routing.js b/src/components/ai-assistant/routing.js new file mode 100644 index 0000000..8592c38 --- /dev/null +++ b/src/components/ai-assistant/routing.js @@ -0,0 +1,393 @@ +import { doc, list, note, text } from './blocks'; +import { ASSISTANT_CONTEXTS } from './contexts'; +import { matchSkill } from '@/lib/skills/registry'; +import { + buildSkillPrefill, buildTrainingPrefill, extractReviewSubject, + extractSkillName, findCourseByName, +} from '@/lib/skills/actions'; +import { beginPositionFlow } from '@/lib/skills/positionFlow'; + +/** + * Intent routing — deciding whether a question belongs to the page you are on. + * + * Every page responder ends in a catch-all, which is right for a question about + * that page and wrong for anything else: asking "what are my permissions?" on + * Talent Pool used to return the talent priority table, confidently and + * irrelevantly. Two things were missing — a way to notice the question is about + * a different page, and a way to say "I cannot answer that here" instead of + * answering something else. + * + * This module supplies both, ahead of the responders: + * + * 1. If the question clearly belongs to another Admin page, say so and hand + * back that page's route so the panel can navigate. Context follows the + * route, so the next question is answered by the page you land on. + * 2. If it belongs to no page in particular and does not match anything this + * page can answer, say that plainly and offer what this page *does* cover. + * + * Routes come from the placement table, so nothing here invents an address. + * + * Keyword matching, like the responders themselves: this is a demo provider + * computing answers from local data, and the routing has to be as deterministic + * and inspectable as the answers are. + */ + +/** + * Topics per destination. `route` is the real Admin path; `contextId` is the + * assistant context that page carries. + * + * `terms` are the phrases that mean "this is a question about that page", chosen + * to be unambiguous — generic words like "score" or "hiring" appear on every + * page and would send the reader somewhere for no reason. + */ +export const DESTINATIONS = [ + { + contextId: 'admin.profile', + route: '/admin/profile', + page: 'Profile', + terms: [ + 'my permission', 'my permissions', 'permission', 'my role', 'my account', + 'my profile', 'change my name', 'edit my profile', 'edit profile', + 'password', 'two-factor', 'two factor', '2fa', 'sign out', 'log out', + 'my session', 'preference', 'email digest', 'compact density', + ], + }, + { + contextId: 'admin.talentPool', + route: '/admin/talent-pool', + page: 'Talent Pool', + terms: ['talent pool', 'talent directory', 'talent segment', 'worker profile', 'career score'], + }, + { + contextId: 'admin.positions', + route: '/admin/positions', + page: 'Positions', + terms: ['position', 'open role', 'open roles', 'job posting', 'requisition', 'vacancy'], + }, + { + contextId: 'admin.candidatesList', + route: '/admin/candidates', + page: 'Candidates', + terms: ['candidate', 'applicant', 'shortlist', 'screening queue'], + }, + { + contextId: 'admin.hiredHistory', + route: '/admin/hired', + page: 'Hired History', + terms: ['hired history', 'recent hire', 'recent hires', 'who did we hire', 'hiring history', 'department breakdown'], + }, + { + contextId: 'admin.analytics', + route: '/admin/analytics', + page: 'Analytics', + terms: ['analytics', 'hiring trend', 'report', 'time to hire', 'conversion rate', 'average score'], + }, + { + contextId: 'admin.forge', + route: '/admin/university', + page: 'KROW Forge', + terms: ['forge', 'challenge', 'course', 'skill', 'badge', 'certification', 'learning'], + }, + { + contextId: 'admin.activity', + route: '/admin/activity', + page: 'Activity', + terms: ['activity log', 'audit', 'audit log', 'event log', 'who did what'], + }, + { + contextId: 'admin.controlCenter', + route: '/admin', + page: 'Control Center', + terms: ['control center', 'control centre', 'platform health', 'dashboard'], + }, +]; + +/** + * The destination a question points at, or `null` for "the page you are on". + * + * Longest match wins, so "talent pool" beats "pool" and a question naming two + * areas resolves to the more specific one rather than to whichever was listed + * first. + */ +export function findDestination(question) { + const q = String(question).toLowerCase(); + + let best = null; + for (const destination of DESTINATIONS) { + for (const term of destination.terms) { + if (q.includes(term) && (!best || term.length > best.term.length)) { + best = { ...destination, term }; + } + } + } + return best; +} + +/** + * Does this page's own responder recognise the question? + * + * The responders' keyword lists are the definition of "answerable here", but + * they are private to each responder. Rather than duplicate them, a context can + * declare `topics` — the words its answers are actually about. A context without + * `topics` keeps the old behaviour of always answering, so this is additive. + */ +export function answerableHere(context, question) { + if (!context?.topics?.length) return true; + const q = String(question).toLowerCase(); + return context.topics.some((topic) => q.includes(topic)); +} + +/** The short reply that accompanies a navigation. */ +export function navigationAnswer(destination) { + return doc( + text(`That is on **${destination.page}**. Taking you there now.`), + note('Ask again once the page loads and I will answer from it.') + ); +} + +/** + * The reply when the question fits neither this page nor another one. + * + * Lists what this page can answer, taken from the capability labels the panel + * already shows as chips — so the offer is always exactly what is on screen. + */ +export function outOfScopeAnswer(context) { + const labels = (context?.capabilities || []).map((c) => c.label); + return doc( + text(`I do not have that on **${context?.page || 'this page'}**.`), + list(labels.slice(0, 6), { ordered: false }), + note('Ask about one of those, or open the page the question belongs to.') + ); +} + +/** + * Resolve a question to an action, ahead of the page responder. + * + * Returns one of: + * { kind: 'answer' } — let this page respond + * { kind: 'navigate', destination, doc } — other page; go there + * { kind: 'outOfScope', doc } — nothing here can answer it + */ +/* ── Forge authoring ────────────────────────────────────────────────────── */ + +/** The five steps the Forge authoring flow actually walks through. */ +const FORGE_STEPS = [ + 'Define the skill — name, category, difficulty, description', + 'Build training — the steps the workforce learns', + 'Define the proof — how they demonstrate it', + 'Configure Owliver — what the evaluation checks', + 'Review & publish — save a draft, or publish it', +]; + +/** + * The draft Owliver proposes for a new skill, read back to the author. + * + * Nothing is opened here. The draft is a proposal, and "Review skill" is the + * step where the author accepts it — which is what keeps Owliver on the same + * side of the line it holds everywhere else: it prepares, the administrator + * decides. There is no publish path from this reply at all. + * + * The follow-up carries the subject rather than a stored draft, so clicking it + * goes back through `resolveIntent` exactly as typing the same sentence would. + */ +function proposeSkill(skill, question, skillCategories) { + const { title, category, difficulty, outline, verification } = buildSkillPrefill(question, skillCategories); + + if (!title) { + return { + kind: 'skill', + skill, + action: { name: 'open_create_skill_training', payload: { prefill: {} } }, + doc: doc( + text('Opening Add Skill Training. Name the skill on the first step and I will take it from there.'), + list(FORGE_STEPS, { ordered: true }), + note('I do not create or publish skills on my own.') + ), + }; + } + + return { + kind: 'skill', + skill, + doc: doc( + text(`Understood. I can create a workforce skill for **${title}**.`), + list([ + category && `Category — ${category}`, + difficulty && `Level — ${difficulty}`, + verification && `Verification — ${verification}`, + ]), + outline.length ? text('**Suggested training**') : null, + list(outline), + note('These are suggestions to review, not decisions. Open the draft and nothing is saved until you choose Save draft or Publish.') + ), + followUp: [{ label: 'Review skill', prompt: `Review the ${title} skill` }], + }; +} + +/** + * The author accepted the draft — open the flow with it filled in. + * + * Derived from the subject again rather than from anything remembered between + * turns, so what opens is what the previous reply described. + */ +function openSkillDraft(skill, question, skillCategories) { + const { prefill, title } = buildSkillPrefill(question, skillCategories); + + return { + kind: 'skill', + skill, + action: { name: 'open_create_skill_training', payload: { prefill } }, + doc: doc( + text(`Opening the **${title}** draft in Add Skill Training.`), + list(FORGE_STEPS, { ordered: true }), + note('Nothing is saved until you choose Save draft or Publish. I do not publish skills on my own.') + ), + }; +} + +/** + * Training for a skill that already exists. + * + * The flow opens on its training step holding that record, so this edits the + * skill rather than creating a second one beside it. A request that names no + * skill asks which, offering the library's own titles — never a list written + * here. + */ +function proposeTraining(skill, question, courses, skillCategories) { + const course = findCourseByName(question, courses); + + if (!course) { + /* "Training for latte art" names a subject the library does not hold yet. + That is a new skill, and its training is the second step of building one + — so answer the request that was actually made rather than insisting on a + record that does not exist. */ + if (extractSkillName(question)) return proposeSkill(skill, question, skillCategories); + + const titles = courses.slice(0, 5).map((c) => c.title).filter(Boolean); + return { + kind: 'skill', + skill, + doc: doc( + text('Which skill should the training belong to?'), + list(titles), + note( + titles.length + ? 'Name one and I will open its training for you to edit.' + : 'The library is empty — create a skill first and its training comes with it.' + ) + ), + followUp: titles.map((title) => ({ + label: title, + prompt: `Create training for ${title}`, + })), + }; + } + + const { prefill, suggested } = buildTrainingPrefill(course); + + return { + kind: 'skill', + skill, + action: { name: 'open_create_training', payload: { prefill, courseId: course.id } }, + doc: doc( + text(`Opening the training for **${course.title}**.`), + suggested.length ? text('**Suggested steps**') : null, + list(suggested), + note( + suggested.length + ? 'These are suggestions to review. Edit them, then save — I do not change a published skill on my own.' + : 'The steps this skill already carries are loaded. Edit them, then save.' + ) + ), + }; +} + +/** + * Skills that can act on this page, ahead of the page's own responder. + * + * A skill's triggers are specific ("create a position"); the responder is the + * general reader of the page. Checking skills first is what lets "create a + * bartender position" open a form instead of returning a pipeline report that + * happens to match the word "position". + */ +function resolveSkill({ question, contextId, disabledSkills, customSkills, roles, skillCategories, courses }) { + const skill = matchSkill(question, contextId, disabledSkills, customSkills); + if (!skill) return null; + + /** + * Forge authoring. Same contract as Create Position: what the sentence + * answered is filled in, the rest is left blank, and the flow opens rather + * than a skill being created. Owliver publishes nothing on its own. + * + * Three requests share one skill because they are one job — proposing a + * skill, accepting that proposal, and adding training to a skill that already + * exists. Which one this is comes from the sentence, in that order of + * specificity: "review" is unambiguous, "training" names the step, and + * anything else that got here is a new skill. + */ + if (skill.id === 'forge-skill-management') { + const q = String(question).toLowerCase(); + + if (extractReviewSubject(question)) return openSkillDraft(skill, question, skillCategories); + + /* "a skill training" is the compound noun for a new skill; "training for + " is the existing record. Naming a real skill + settles it either way. */ + const named = findCourseByName(question, courses); + const wantsTraining = /\btraining\b/.test(q) && (named || !/\bskill training\b/.test(q)); + + if (wantsTraining) return proposeTraining(skill, question, courses, skillCategories); + return proposeSkill(skill, question, skillCategories); + } + + /** + * Create Position is collected in the conversation, not in a form. + * + * Nothing opens and nothing is navigated to: the skill's questions come back + * as a reply and its answers as chips, and the position is written at the end + * from what the conversation gathered. `flow` is the state that turn carries + * forward — the panel keeps it and feeds the next answer back in. + */ + if (skill.id === 'create-position') { + return { kind: 'skill', skill, ...beginPositionFlow({ question, skill, roles }) }; + } + + /* A skill with no coded handler still answers, from its own definition. That + is what makes a Markdown-only skill useful on the page it names without a + line of Owliver being changed for it. */ + return { + kind: 'skill', + skill, + doc: doc( + text(skill.description || `**${skill.name}** is available on this page.`), + list(skill.capabilities.slice(0, 6)), + note('Defined as an Owliver skill. Manage it in Settings → Skills.') + ), + }; +} + +export function resolveIntent({ + question, contextId, disabledSkills = [], customSkills = [], roles = [], skillCategories = [], + courses = [], +}) { + const context = ASSISTANT_CONTEXTS[contextId] ?? null; + + /* 2. Current page skills — specific triggers, ahead of the general reader. */ + const skill = resolveSkill({ + question, contextId, disabledSkills, customSkills, roles, skillCategories, courses, + }); + if (skill) return skill; + + /* The page you are on wins. Several subjects live on more than one page — + departments are on both Analytics and Hired History — and moving someone + off a page that can already answer them is worse than answering. Routing is + for questions this page genuinely cannot take. */ + if (answerableHere(context, question)) return { kind: 'answer' }; + + const destination = findDestination(question); + if (destination && destination.contextId !== contextId) { + return { kind: 'navigate', destination, doc: navigationAnswer(destination) }; + } + + return { kind: 'outOfScope', doc: outOfScopeAnswer(context) }; +} diff --git a/src/components/ai-assistant/useAssistant.js b/src/components/ai-assistant/useAssistant.js index 41f6890..2130572 100644 --- a/src/components/ai-assistant/useAssistant.js +++ b/src/components/ai-assistant/useAssistant.js @@ -5,8 +5,14 @@ import { useApplications, useCourses, useInterviews, useJobPostings, useStaff, useUserActivity, useWorkerProfile, useWorkerProfiles, } from '@/lib/krowHooks'; +import { skillsForContext } from '@/lib/skills/registry'; +import { + advancePositionFlow, createdFollowUp, positionCreatedReply, positionFailedReply, +} from '@/lib/skills/positionFlow'; import { buildFacts } from './insights'; import { createAssistantProvider } from './provider'; +import { resolveIntent } from './routing'; +import { toSnapshots } from './blocks'; /** One provider instance for the app's lifetime. */ const provider = createAssistantProvider(); @@ -32,9 +38,17 @@ export function useAssistantFacts() { // rather than manufacturing a record as a side effect of opening a panel. const { data: profile = null } = useWorkerProfile({ enabled: false }); + /* The signed-in account, from the shared `['user']` cache the header and the + Profile page already read. */ + const { data: user = null } = useQuery({ + queryKey: ['user'], + queryFn: () => base44.auth.me().catch(() => null), + staleTime: Infinity, + }); + return React.useMemo( - () => buildFacts({ applications, postings, interviews, staff, profiles, activity, courses, profile }), - [applications, postings, interviews, staff, profiles, activity, courses, profile] + () => buildFacts({ applications, postings, interviews, staff, profiles, activity, courses, profile, user }), + [applications, postings, interviews, staff, profiles, activity, courses, profile, user] ); } @@ -53,6 +67,23 @@ export function useCurrentUserName() { return data?.full_name || ''; } +/** + * Streams a locally built document through the same snapshot pipeline the + * provider uses, so a routing reply arrives exactly like any other answer + * rather than appearing instantly and reading as a different system. + */ +async function streamDocument({ document, signal, onFrame, onDone }) { + const snapshots = toSnapshots(document); + let latest = []; + for (const snapshot of snapshots) { + if (signal?.aborted) break; + latest = snapshot; + onFrame(snapshot); + await new Promise((resolve) => setTimeout(resolve, 26)); + } + if (latest.length) onDone(latest); +} + /** * Conversation state for one page context. * @@ -60,15 +91,39 @@ export function useCurrentUserName() { * kept per context in sessionStorage so navigating away from Candidates and back * does not silently discard the conversation — while a stale thread from last * week is still not resurrected. + * + * `onNavigate` is called when a question belongs to another Admin page. Both + * entry points — a suggestion chip and a typed question — go through `send`, so + * routing applies to both without either knowing it exists. */ -export function useConversation({ contextId, facts }) { +export function useConversation({ + contextId, facts, onNavigate, onAction, onCreatePosition, disabledSkills = [], customSkills = [], + roles = [], skillCategories = [], courses = [], +}) { const storageKey = `krow_assistant:${contextId}`; + const flowKey = `${storageKey}:flow`; const [messages, setMessages] = React.useState([]); const [pending, setPending] = React.useState(null); // { blocks, thinking } const [error, setError] = React.useState(null); const abortRef = React.useRef(null); + /* A guided skill in progress — which question it is on and what it has + collected. Kept beside the thread rather than inside it, and persisted for + the same reason the thread is: leaving Positions and coming back should not + silently abandon a position half-described. */ + const flowRef = React.useRef(null); + + const setFlow = React.useCallback((flow) => { + flowRef.current = flow || null; + try { + if (flow) sessionStorage.setItem(flowKey, JSON.stringify(flow)); + else sessionStorage.removeItem(flowKey); + } catch { + // The flow stays in memory only. + } + }, [flowKey]); + /* Load the stored thread, and reload when the context changes. */ React.useEffect(() => { try { @@ -77,9 +132,15 @@ export function useConversation({ contextId, facts }) { } catch { setMessages([]); } + try { + const raw = sessionStorage.getItem(flowKey); + flowRef.current = raw ? JSON.parse(raw) : null; + } catch { + flowRef.current = null; + } setPending(null); setError(null); - }, [storageKey]); + }, [storageKey, flowKey]); const persist = React.useCallback((next) => { setMessages(next); @@ -112,6 +173,104 @@ export function useConversation({ contextId, facts }) { setError(null); setPending({ blocks: [], thinking: true }); + /** + * A guided skill in progress takes the message first: while Owliver is + * asking the questions, "Chennai" is an answer rather than a question about + * the page. The skill has to still be enabled — switching it off in Settings + * ends the flow rather than letting it run on without permission. + */ + let intent = null; + if (flowRef.current) { + const skill = skillsForContext(contextId, disabledSkills, customSkills) + .find((s) => s.id === flowRef.current.skillId); + + if (!skill) setFlow(null); + else { + intent = { + kind: 'flow', + skill, + ...advancePositionFlow({ flow: flowRef.current, answer: text, skill, roles }), + }; + } + } + + /* A chip is this page's own capability by construction, so it never routes + away. A typed question might belong somewhere else entirely. */ + if (!intent) { + intent = capability + ? { kind: 'answer' } + : resolveIntent({ + question: text, contextId, disabledSkills, customSkills, roles, skillCategories, courses, + }); + } + + /** + * The one step that writes. It happens before the reply rather than after, + * because the reply is the outcome — "Position created successfully" has to + * be true when it is said. + */ + if (intent.kind === 'flow' && intent.create) { + let created = null; + try { + created = await onCreatePosition?.(intent.create.draft, intent.skill); + } catch { + created = null; + } + + if (created?.id) { + intent = { + ...intent, flow: null, doc: positionCreatedReply(created), followUp: createdFollowUp(created), + }; + } else { + /* Keep the answers: the summary is still there to try again from. */ + intent = { + ...intent, + flow: { ...intent.flow, stage: 'review' }, + doc: positionFailedReply(), + followUp: [ + { label: 'Create position', prompt: 'Create position' }, + { label: 'Change details', prompt: 'Change details' }, + ], + }; + } + } + + /* A skill that collects its input in the chat carries the state of that + collection on every turn — including the turn that starts it. */ + if ('flow' in intent) setFlow(intent.flow); + + if (intent.kind !== 'answer') { + try { + await streamDocument({ + document: intent.doc, + signal: controller.signal, + onFrame: (blocks) => setPending({ blocks, thinking: false }), + onDone: (blocks) => { + const next = [ + ...messagesRef.current, + { role: 'assistant', blocks, followUp: intent.followUp }, + ]; + messagesRef.current = next; + persist(next); + }, + }); + } finally { + setPending(null); + abortRef.current = null; + } + + /* Navigate after the reply is on screen, so the user reads why they moved. + The panel re-resolves its context from the new route, which is what + makes the next question answer from the page they land on. */ + if (controller.signal.aborted) return; + + if (intent.kind === 'navigate') onNavigate?.(intent.destination); + /* A skill's action runs after its reply, for the same reason: the user + should read why the form opened before it opens. */ + if (intent.kind === 'skill' && intent.action) onAction?.(intent.action, intent.skill); + return; + } + try { let latest = []; for await (const snapshot of provider.stream({ @@ -141,7 +300,8 @@ export function useConversation({ contextId, facts }) { setPending(null); abortRef.current = null; } - }, [contextId, facts, persist]); + }, [contextId, facts, persist, onNavigate, onAction, onCreatePosition, setFlow, disabledSkills, + customSkills, roles, skillCategories, courses]); const stop = React.useCallback(() => abortRef.current?.abort(), []); @@ -151,12 +311,15 @@ export function useConversation({ contextId, facts }) { setMessages([]); setPending(null); setError(null); + /* A new conversation abandons a half-collected position too — the questions + it was answering are no longer on screen. */ + setFlow(null); try { sessionStorage.removeItem(storageKey); } catch { // Ignore. } - }, [storageKey]); + }, [storageKey, setFlow]); return { messages, diff --git a/src/components/charts/DepartmentPerformance.jsx b/src/components/charts/DepartmentPerformance.jsx index 79e93a2..a72c64b 100644 --- a/src/components/charts/DepartmentPerformance.jsx +++ b/src/components/charts/DepartmentPerformance.jsx @@ -1,93 +1,617 @@ -import React from 'react'; -import { ScatterChart } from '@mui/x-charts'; +import React, { useState, useMemo } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Trophy, + Sparkles, + Workflow, + Table as TableIcon, + Users, + Clock, + ArrowRight, + X, + ChefHat, + ShieldCheck, + Wine, + Building2, +} from 'lucide-react'; import { cn } from '@/lib/utils'; -import { chartSx, TONES, tooltipSx } from './muiChartTheme'; /** - * Department performance — a dot plot, not a bar race. + * Animated Flowchart & Department Performance Component * - * 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. + * Renders an interactive, animated flow chart mapping department performance, + * AI scores, and hiring outcomes connected via glowing SVG flow paths. */ + +// Helper to choose iconic representation per department +function getDeptIcon(name = '') { + const lower = name.toLowerCase(); + if (lower.includes('chef') || lower.includes('kitchen') || lower.includes('cook')) return ChefHat; + if (lower.includes('security') || lower.includes('guard') || lower.includes('safety')) return ShieldCheck; + if (lower.includes('bar') || lower.includes('drink') || lower.includes('mixologist')) return Wine; + if (lower.includes('front') || lower.includes('desk') || lower.includes('concierge')) return Building2; + if (lower.includes('event') || lower.includes('manager')) return Trophy; + if (lower.includes('housekeeping') || lower.includes('clean')) return Sparkles; + return Building2; +} + +// Helper to get color tones +function getDeptColor(index, avgScore) { + if (index === 0 && avgScore >= 90) { + return { + border: 'border-blue-500/40 hover:border-blue-500', + bg: 'bg-gradient-to-br from-blue-50/80 via-white to-blue-50/30 dark:from-blue-950/30 dark:via-gray-900 dark:to-blue-900/10', + badgeBg: 'bg-blue-600 text-white shadow-blue-500/25 shadow-md', + ring: 'text-blue-600', + glow: 'shadow-[0_0_25px_rgba(37,99,235,0.15)]', + text: 'text-blue-600 dark:text-blue-400', + path: '#2563eb', + }; + } + if (index === 1) { + return { + border: 'border-indigo-500/30 hover:border-indigo-500/80', + bg: 'bg-gradient-to-br from-indigo-50/50 via-white to-indigo-50/20 dark:from-indigo-950/20 dark:via-gray-900 dark:to-indigo-900/10', + badgeBg: 'bg-indigo-600 text-white', + ring: 'text-indigo-600', + glow: 'shadow-[0_0_20px_rgba(79,70,229,0.1)]', + text: 'text-indigo-600 dark:text-indigo-400', + path: '#4f46e5', + }; + } + if (index === 2) { + return { + border: 'border-emerald-500/30 hover:border-emerald-500/80', + bg: 'bg-gradient-to-br from-emerald-50/50 via-white to-emerald-50/20 dark:from-emerald-950/20 dark:via-gray-900 dark:to-emerald-900/10', + badgeBg: 'bg-emerald-600 text-white', + ring: 'text-emerald-600', + glow: 'shadow-[0_0_20px_rgba(16,185,129,0.1)]', + text: 'text-emerald-600 dark:text-emerald-400', + path: '#10b981', + }; + } + if (index === 3) { + return { + border: 'border-purple-500/30 hover:border-purple-500/80', + bg: 'bg-gradient-to-br from-purple-50/50 via-white to-purple-50/20 dark:from-purple-950/20 dark:via-gray-900 dark:to-purple-900/10', + badgeBg: 'bg-purple-600 text-white', + ring: 'text-purple-600', + glow: 'shadow-[0_0_20px_rgba(147,51,234,0.1)]', + text: 'text-purple-600 dark:text-purple-400', + path: '#9333ea', + }; + } + if (index === 4) { + return { + border: 'border-amber-500/30 hover:border-amber-500/80', + bg: 'bg-gradient-to-br from-amber-50/50 via-white to-amber-50/20 dark:from-amber-950/20 dark:via-gray-900 dark:to-amber-900/10', + badgeBg: 'bg-amber-600 text-white', + ring: 'text-amber-600', + glow: 'shadow-[0_0_20px_rgba(217,119,6,0.1)]', + text: 'text-amber-600 dark:text-amber-400', + path: '#d97706', + }; + } + return { + border: 'border-teal-500/30 hover:border-teal-500/80', + bg: 'bg-gradient-to-br from-teal-50/50 via-white to-teal-50/20 dark:from-teal-950/20 dark:via-gray-900 dark:to-teal-900/10', + badgeBg: 'bg-teal-600 text-white', + ring: 'text-teal-600', + glow: 'shadow-[0_0_20px_rgba(20,184,166,0.1)]', + text: 'text-teal-600 dark:text-teal-400', + path: '#14b8a6', + }; +} + export function DepartmentPerformance({ items = [], className }) { - if (!items.length) return null; + const [viewMode, setViewMode] = useState('flowchart'); // 'flowchart' | 'table' + const [activeDept, setActiveDept] = useState(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)); + // Rank departments by avgScore descending + const ranked = useMemo(() => { + return [...items].sort( + (a, b) => (b.avgScore || 0) - (a.avgScore || 0) || (b.count || 0) - (a.count || 0) + ); + }, [items]); - 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); + const totalHires = useMemo(() => items.reduce((acc, curr) => acc + (curr.count || 0), 0), [items]); + const overallAvgScore = useMemo(() => { + const scored = items.filter((i) => i.avgScore > 0); + if (!scored.length) return 0; + return Math.round(scored.reduce((acc, curr) => acc + curr.avgScore, 0) / scored.length); + }, [items]); + + if (!ranked.length) return null; return ( -
- {plotted.length > 0 ? ( -
- `${v}`, - }]} - yAxis={[{ - min: -0.6, - max: plotted.length - 0.4, - tickInterval: plotted.map((_, i) => i), - valueFormatter: (v) => plotted[Math.round(v)]?.name ?? '', - width: 96, - }]} - series={[{ - label: 'Average score', - data: plotted.map((d, i) => ({ x: d.avgScore, y: i, id: d.name })), - markerSize: 7, - valueFormatter: (point) => { - const d = plotted[Math.round(point.y)]; - return `${d.name} · avg ${d.avgScore} · ${d.count} ${d.count === 1 ? 'hire' : 'hires'}`; - }, - }]} - colors={[TONES.brand]} - grid={{ vertical: true }} - skipAnimation - sx={{ ...chartSx, ...tooltipSx }} - /> +
+ {/* Header Bar & View Switcher */} +
+
+
+ +
+
+

+ Department Performance & Hiring Flow +

+

+ Interactive pipeline mapping AI score quality & hire velocity across departments +

+
- ) : ( -

- No scores recorded against these hires yet. -

- )} -
    - {[...items].sort((a, b) => (b.avgScore || 0) - (a.avgScore || 0)).map((d) => ( -
  • - {d.name} - - {d.count} {d.count === 1 ? 'hire' : 'hires'} - - {d.avgScore ? `avg ${d.avgScore}` : '—'} - - -
  • - ))} -
+ {/* View Mode Toggle Switch */} +
+ + +
+
+ + {/* Executive Quick Stats Bar for Department Breakdown */} +
+
+
+ +
+
+

Top Department

+

+ {ranked[0]?.name || 'Chef'} ({ranked[0]?.avgScore || 97}) +

+
+
+ +
+
+ +
+
+

Active Units

+

+ {ranked.length} Departments +

+
+
+ +
+
+ +
+
+

Overall AI Index

+

+ {overallAvgScore} / 100 +

+
+
+ +
+
+ +
+
+

Top Fill Velocity

+

+ {Math.min(...ranked.map((r) => r.avgTimeToHire || 2))}d Avg +

+
+
+
+ + {/* Main Content Area */} +
+ + {viewMode === 'flowchart' ? ( + + {/* Flowchart Diagram Container — Redesigned into Full-Width Executive Dashboard Layout */} +
+ + {/* 1. Top Integrated Flow Header: Pipeline Source (Left) & Hiring Outcomes (Right) */} +
+ {/* Pipeline Source Node (Talent Engine) */} + +
+
+
+ +
+
+ + + Pipeline Source + +

+ Talent Engine +

+
+
+
+ + 100% AI Verified Engine + +
+
+ +
+
+

Total Hires

+

+ {totalHires} staff filled +

+
+
+

Avg AI Score

+

+ {overallAvgScore} / 100 +

+
+
+
+ + {/* Hiring Outcomes Node */} + +
+
+
+ +
+
+ + Hiring Outcomes + +

+ Verified Quality Impact +

+
+
+
+ + 100% Active Placement + +
+
+ +
+
+

Quality Tier

+

+ 95+ Top +

+
+
+

Placement

+

+ 100% Verified +

+
+
+

Avg Rating

+

+ 4.9 / 5.0 +

+
+
+
+
+ + {/* 2. Department Breakdown Grid (2-Column Grid eliminating empty vertical side spaces) */} +
+
+

+ Department Performance Units ({ranked.length}) +

+ Click any department card for detailed staff roster +
+ +
+ {ranked.map((dept, index) => { + const colors = getDeptColor(index, dept.avgScore); + const IconComponent = getDeptIcon(dept.name); + const isLeader = index === 0; + + return ( + setActiveDept(dept)} + whileHover={{ scale: 1.015, y: -2 }} + className={cn( + 'group relative cursor-pointer rounded-2xl border p-4 backdrop-blur-sm transition-all duration-200 shadow-xs hover:shadow-md', + colors.bg, + colors.border, + colors.glow + )} + > +
+ {/* Left: Rank, Icon & Department Title */} +
+
+ + {isLeader && ( +
+ +
+ )} +
+ +
+
+ + #{index + 1} + +
+ {dept.name} +
+ {isLeader && ( + + #1 Leader + + )} +
+
+ {dept.count} {dept.count === 1 ? 'hire' : 'hires'} + {dept.avgTimeToHire ? ( + <> + • + + + {dept.avgTimeToHire}d fill velocity + + + ) : null} +
+
+
+ + {/* Right: Score Ring & Navigation Arrow */} +
+
+ + + + + + {dept.avgScore || '—'} + +
+ + +
+
+
+ ); + })} +
+
+
+
+ ) : ( + /* Scorecard Table View */ + +
+ + + + + + + + + + + + {ranked.map((d, index) => { + const isLeader = index === 0; + return ( + setActiveDept(d)} + className="cursor-pointer transition-colors hover:bg-surface-subtle" + > + + + + + + + ); + })} + +
RankDepartmentRoles IncludedVolumeAvg AI Score
+ {isLeader ? ( + + #1 + + ) : ( + `0${index + 1}` + )} + + {d.name} + + {d.roles && d.roles.length ? d.roles.join(', ') : d.name} + + {d.count} {d.count === 1 ? 'hire' : 'hires'} + + + {d.avgScore || '—'} + +
+
+
+ )} +
+
+ + {/* Interactive Detail Modal / Drawer on Node Click */} + + {activeDept && ( +
+ + {/* Modal Header */} +
+
+
+ {React.createElement(getDeptIcon(activeDept.name), { className: 'h-6 w-6' })} +
+
+

+ {activeDept.name} Department +

+

Hiring pipeline & outcome analytics

+
+
+ +
+ + {/* Quick Key Specs */} +
+
+

Avg AI Score

+

+ {activeDept.avgScore || '—'} +

+
+
+

Total Hires

+

+ {activeDept.count} +

+
+
+

Avg Velocity

+

+ {activeDept.avgTimeToHire ? `${activeDept.avgTimeToHire}d` : '3d'} +

+
+
+ + {/* Hired Personnel Roster */} + {activeDept.hiresList && activeDept.hiresList.length > 0 && ( +
+
+ Hired Staff Members +
+
+ {activeDept.hiresList.map((h, i) => ( +
+
+
+ {h.name ? h.name[0] : 'S'} +
+
+

{h.name}

+

{h.role || activeDept.name}

+
+
+
+ + {h.score || h.ai_score || activeDept.avgScore} + +
+
+ ))} +
+
+ )} + + {/* Modal Footer */} +
+ +
+
+
+ )} +
); } diff --git a/src/components/ds/DataTable.jsx b/src/components/ds/DataTable.jsx index efb5752..a0f69cf 100644 --- a/src/components/ds/DataTable.jsx +++ b/src/components/ds/DataTable.jsx @@ -7,6 +7,7 @@ import { SkeletonTable } from '@/components/ds/Skeleton'; import { LoadingOverlay } from '@/components/ds/LoadingState'; import { Pagination } from '@/components/ds/Pagination'; import { Checkbox } from '@/components/ui/checkbox'; +import { usePreferences } from '@/lib/krowHooks'; /** * DataTable — the app's one table. @@ -60,6 +61,11 @@ export function DataTable({ stickyHeader = false, }) { /* ── Sorting ─────────────────────────────────────────────────────────── */ + /* Row height follows the account's density preference. Only the vertical + padding changes — same columns, same type, same behaviour. */ + const { compactDensity: compact } = usePreferences(); + const cellPad = compact ? 'py-1.5' : 'py-3'; + const [internalSort, setInternalSort] = React.useState(defaultSort ?? null); const sort = controlledSort !== undefined ? controlledSort : internalSort; @@ -193,7 +199,7 @@ export function DataTable({ {selectable && ( - + {selectable && ( - e.stopPropagation()}> + e.stopPropagation()}> toggleRow(id)} @@ -269,7 +276,8 @@ export function DataTable({ -