diff --git a/README.md b/README.md index 61295d00f3ca25e30216e3c4006a24f4c88e32bd..ce689966568c5744ecd08a575b627c50d38818b4 100644 --- a/README.md +++ b/README.md @@ -23,34 +23,28 @@ cp ./.env.template.local ./.env.production.local - [PM2](https://pm2.keymetrics.io/) # Routes -- [ ] check routes - - [x] AUTH - - [x] register - - [x] confirm register - - [x] resend register token - - [x] login - - [x] renew JWT - - [x] logout - - [x] request password reset - - [x] password reset - - [ ] AI - - [ ] status - - [ ] get models - - [ ] get model - - [ ] install model [admin only] - - [ ] delete model [admin only] - - [ ] chat - - [ ] list chats - - [ ] EMBEDDINGS - - [ ] delete vector db [admin only] - - [ ] get vector db [admin only] - - [ ] update embeddings [admin only] +- [x] AUTH + - [x] register + - [x] confirm register + - [x] resend register token + - [x] login + - [x] renew JWT + - [x] logout + - [x] request password reset + - [x] password reset +- [x] AI + - [x] status + - [x] get models + - [x] get model + - [x] install model [admin only] + - [x] delete model [admin only] + - [x] chat + - [x] list chats +- [x] EMBEDDINGS + - [x] status [admin only] + - [x] delete vector db [admin only] + - [x] update embeddings [admin only] # Roadmap -- [ ] complete pages - - [ ] resend verification code - - [ ] onboarding / RAGChat - - [ ] admin-login - - [ ] admin-page with LLM options - [ ] fix errors - [ ] check width of label & submit on cleanLayout \ No newline at end of file diff --git a/README_tmp.html b/README_tmp.html index 2dc272fe0c6d6d4dfd67602bbf5ddf1bf12dfda4..726e54b313e956aa3f6569eb05b15b4ed41a2b87 100644 --- a/README_tmp.html +++ b/README_tmp.html @@ -389,54 +389,42 @@ cp ./.env.template.local ./.env.production.local </ul> <h1 id="routes">Routes</h1> <ul> -<li><input type="checkbox" id="checkbox0"><label for="checkbox0">check routes</label> +<li><input type="checkbox" id="checkbox0" checked="true"><label for="checkbox0">AUTH</label> <ul> -<li><input type="checkbox" id="checkbox1" checked="true"><label for="checkbox1">AUTH</label> -<ul> -<li><input type="checkbox" id="checkbox2" checked="true"><label for="checkbox2">register</label></li> -<li><input type="checkbox" id="checkbox3" checked="true"><label for="checkbox3">confirm register</label></li> -<li><input type="checkbox" id="checkbox4" checked="true"><label for="checkbox4">resend register token</label></li> -<li><input type="checkbox" id="checkbox5" checked="true"><label for="checkbox5">login</label></li> -<li><input type="checkbox" id="checkbox6" checked="true"><label for="checkbox6">renew JWT</label></li> -<li><input type="checkbox" id="checkbox7" checked="true"><label for="checkbox7">logout</label></li> -<li><input type="checkbox" id="checkbox8" checked="true"><label for="checkbox8">request password reset</label></li> -<li><input type="checkbox" id="checkbox9" checked="true"><label for="checkbox9">password reset</label></li> +<li><input type="checkbox" id="checkbox1" checked="true"><label for="checkbox1">register</label></li> +<li><input type="checkbox" id="checkbox2" checked="true"><label for="checkbox2">confirm register</label></li> +<li><input type="checkbox" id="checkbox3" checked="true"><label for="checkbox3">resend register token</label></li> +<li><input type="checkbox" id="checkbox4" checked="true"><label for="checkbox4">login</label></li> +<li><input type="checkbox" id="checkbox5" checked="true"><label for="checkbox5">renew JWT</label></li> +<li><input type="checkbox" id="checkbox6" checked="true"><label for="checkbox6">logout</label></li> +<li><input type="checkbox" id="checkbox7" checked="true"><label for="checkbox7">request password reset</label></li> +<li><input type="checkbox" id="checkbox8" checked="true"><label for="checkbox8">password reset</label></li> </ul> </li> -<li><input type="checkbox" id="checkbox10"><label for="checkbox10">AI</label> +<li><input type="checkbox" id="checkbox9" checked="true"><label for="checkbox9">AI</label> <ul> -<li><input type="checkbox" id="checkbox11"><label for="checkbox11">status</label></li> -<li><input type="checkbox" id="checkbox12"><label for="checkbox12">get models</label></li> -<li><input type="checkbox" id="checkbox13"><label for="checkbox13">get model</label></li> -<li><input type="checkbox" id="checkbox14"><label for="checkbox14">install model [admin only]</label></li> -<li><input type="checkbox" id="checkbox15"><label for="checkbox15">delete model [admin only]</label></li> -<li><input type="checkbox" id="checkbox16"><label for="checkbox16">chat</label></li> -<li><input type="checkbox" id="checkbox17"><label for="checkbox17">list chats</label></li> +<li><input type="checkbox" id="checkbox10" checked="true"><label for="checkbox10">status</label></li> +<li><input type="checkbox" id="checkbox11" checked="true"><label for="checkbox11">get models</label></li> +<li><input type="checkbox" id="checkbox12" checked="true"><label for="checkbox12">get model</label></li> +<li><input type="checkbox" id="checkbox13" checked="true"><label for="checkbox13">install model [admin only]</label></li> +<li><input type="checkbox" id="checkbox14" checked="true"><label for="checkbox14">delete model [admin only]</label></li> +<li><input type="checkbox" id="checkbox15" checked="true"><label for="checkbox15">chat</label></li> +<li><input type="checkbox" id="checkbox16" checked="true"><label for="checkbox16">list chats</label></li> </ul> </li> -<li><input type="checkbox" id="checkbox18"><label for="checkbox18">EMBEDDINGS</label> +<li><input type="checkbox" id="checkbox17" checked="true"><label for="checkbox17">EMBEDDINGS</label> <ul> -<li><input type="checkbox" id="checkbox19"><label for="checkbox19">delete vector db [admin only]</label></li> -<li><input type="checkbox" id="checkbox20"><label for="checkbox20">get vector db [admin only]</label></li> -<li><input type="checkbox" id="checkbox21"><label for="checkbox21">update embeddings [admin only]</label></li> -</ul> -</li> +<li><input type="checkbox" id="checkbox18" checked="true"><label for="checkbox18">status [admin only]</label></li> +<li><input type="checkbox" id="checkbox19" checked="true"><label for="checkbox19">delete vector db [admin only]</label></li> +<li><input type="checkbox" id="checkbox20" checked="true"><label for="checkbox20">update embeddings [admin only]</label></li> </ul> </li> </ul> <h1 id="roadmap">Roadmap</h1> <ul> -<li><input type="checkbox" id="checkbox22"><label for="checkbox22">complete pages</label> -<ul> -<li><input type="checkbox" id="checkbox23"><label for="checkbox23">resend verification code</label></li> -<li><input type="checkbox" id="checkbox24"><label for="checkbox24">onboarding / RAGChat</label></li> -<li><input type="checkbox" id="checkbox25"><label for="checkbox25">admin-login</label></li> -<li><input type="checkbox" id="checkbox26"><label for="checkbox26">admin-page with LLM options</label></li> -</ul> -</li> -<li><input type="checkbox" id="checkbox27"><label for="checkbox27">fix errors</label> +<li><input type="checkbox" id="checkbox21"><label for="checkbox21">fix errors</label> <ul> -<li><input type="checkbox" id="checkbox28"><label for="checkbox28">check width of label & submit on cleanLayout</label></li> +<li><input type="checkbox" id="checkbox22"><label for="checkbox22">check width of label & submit on cleanLayout</label></li> </ul> </li> </ul> diff --git a/package-lock.json b/package-lock.json index cf180e712b315a06b67cd243d0279cb8d654c9f4..2921d794596c6b48a7e2a01b4b6cebdeb8adbe64 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,13 @@ "version": "0.0.0", "dependencies": { "@hookform/resolvers": "^3.6.0", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@tanstack/react-table": "^8.20.1", "axios": "^1.7.2", + "class-variance-authority": "^0.7.0", "hamburger-react": "^2.5.1", + "lucide-react": "^0.424.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", @@ -900,6 +905,44 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.5.tgz", + "integrity": "sha512-8GrTWmoFhm5BsMZOTHeGD2/0FLKLQQHvO/ZmQga4tKempYRLz8aqJGqXVuQgisnMObq2YZ2SgkwctN1LOOxcqA==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.5" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.8.tgz", + "integrity": "sha512-kx62rP19VZ767Q653wsP1XZCGIirkE09E0QUGNYTM/ttbbQHqcGPdSfWFxUyyNLc/W6aoJRBajOSXhP6GXjC0Q==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.5" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", + "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.5.tgz", + "integrity": "sha512-sTcG+QZ6fdEUObICavU+aB3Mp8HY4n14wYHdxK4fXjPmv3PXZZeY5RaguJmGyeH/CJQhX3fqKUtS4qc1LoHwhQ==", + "license": "MIT" + }, "node_modules/@hookform/resolvers": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.6.0.tgz", @@ -1096,6 +1139,556 @@ "node": ">=14" } }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", + "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", + "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", + "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", + "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.1.tgz", + "integrity": "sha512-zysS+iU4YP3STKNS6USvFVqI4qqx8EpiwmT5TuCApVEBca+eRCbONi4EgzfNSuVnOXvC5UPHHMjs8RXO6DH9Bg==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", + "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.1.tgz", + "integrity": "sha512-y8E+x9fBq9qvteD2Zwa4397pUVhYsh9iq44b5RD5qu1GMJWBCBuVg1hMyItbc6+zH00TxGRqd9Iot4wzf3OoBQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", + "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", + "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", + "integrity": "sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.0", + "@radix-ui/react-focus-guards": "1.1.0", + "@radix-ui/react-focus-scope": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.1", + "@radix-ui/react-presence": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.7" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", + "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", + "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", + "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", + "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", + "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==", + "license": "MIT" + }, "node_modules/@remix-run/router": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.17.0.tgz", @@ -1577,6 +2170,39 @@ "@svgr/core": "*" } }, + "node_modules/@tanstack/react-table": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.20.1.tgz", + "integrity": "sha512-PJK+07qbengObe5l7c8vCdtefXm8cyR4i078acWrHbdm8JKw1ES7YpmOtVt9ALUVEEFAHscdVpGRhRgikgFMbQ==", + "license": "MIT", + "dependencies": { + "@tanstack/table-core": "8.20.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/@tanstack/table-core": { + "version": "8.20.1", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.20.1.tgz", + "integrity": "sha512-5Ly5TIRHnWH7vSDell9B/OVyV380qqIJVg7H7R7jU4fPEmOD4smqAX7VRflpYI09srWR8aj5OLD2Ccs1pI5mTg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1633,14 +2259,14 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1651,7 +2277,7 @@ "version": "18.3.0", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/react": "*" @@ -1782,6 +2408,18 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", @@ -2207,6 +2845,27 @@ "node": ">= 6" } }, + "node_modules/class-variance-authority": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.0.tgz", + "integrity": "sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "2.0.0" + }, + "funding": { + "url": "https://joebell.co.uk" + } + }, + "node_modules/class-variance-authority/node_modules/clsx": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.0.0.tgz", + "integrity": "sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2328,7 +2987,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/data-view-buffer": { @@ -2455,6 +3114,12 @@ "node": ">=0.4.0" } }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -3385,6 +4050,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/get-symbol-description": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", @@ -4316,6 +4990,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.424.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.424.0.tgz", + "integrity": "sha512-x2Nj2aytk1iOyHqt4hKenfVlySq0rYxNeEf8hE0o+Yh0iE36Rqz0rkngVdv2uQtjZ70LAE73eeplhhptYt9x4Q==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5143,6 +5826,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-remove-scroll": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", + "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.4", + "react-style-singleton": "^2.2.1", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.0", + "use-sidecar": "^1.1.2" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", + "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-router": { "version": "6.24.0", "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.24.0.tgz", @@ -5175,6 +5905,29 @@ "react-dom": ">=16.8" } }, + "node_modules/react-style-singleton": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", + "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "invariant": "^2.2.4", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/react-toastify": { "version": "10.0.5", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", @@ -5992,7 +6745,6 @@ "version": "2.6.3", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true, "license": "0BSD" }, "node_modules/type-check": { @@ -6155,6 +6907,49 @@ "punycode": "^2.1.0" } }, + "node_modules/use-callback-ref": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", + "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", + "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index c775795dce78b41586d151303469ab2a004c8231..9420af1afd1c5a195c368ef082c31e773c60b15c 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,13 @@ }, "dependencies": { "@hookform/resolvers": "^3.6.0", + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@tanstack/react-table": "^8.20.1", "axios": "^1.7.2", + "class-variance-authority": "^0.7.0", "hamburger-react": "^2.5.1", + "lucide-react": "^0.424.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-helmet-async": "^2.0.5", diff --git a/src/components/boxes/ConfirmBox.jsx b/src/components/boxes/ConfirmBox.jsx index 1df510d3685bf83e9795cd24127fd7d7254c239d..b64f3ed55a0e483671bb6b525e956dc7b088c34e 100644 --- a/src/components/boxes/ConfirmBox.jsx +++ b/src/components/boxes/ConfirmBox.jsx @@ -8,7 +8,7 @@ import { DialogHeader, DialogTitle, DialogTrigger, -} from "@/components/ui/dialog"; +} from "/src/components/ui/dialog"; import { Button } from '../ui/button'; import Heading from '../font/Heading'; diff --git a/src/components/boxes/InfoBox.jsx b/src/components/boxes/InfoBox.jsx new file mode 100755 index 0000000000000000000000000000000000000000..309d02b1980ab148d3d75a5fd136ac9f1ed928dc --- /dev/null +++ b/src/components/boxes/InfoBox.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "/src/components/ui/dialog"; +import { Button } from '../ui/button'; +import JsonToHtmlDL from './JsonToHtmlDL'; + +function InfoBox({ infoDialog, closeDialog, item, ...props }) { + // ################################# + // HOOKS + // ################################# + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <Dialog open={infoDialog.open} onOpenChange={closeDialog}> + <DialogContent className="max-w-[95%] grid-rows-[auto_minmax(0,1fr)_auto] p-0 max-h-[90dvh]"> + <DialogHeader className='p-6 pb-0'> + <DialogTitle>{infoDialog.title}</DialogTitle> + <DialogDescription> + {infoDialog.description} + </DialogDescription> + </DialogHeader> + <div className="grid gap-4 py-4 overflow-y-auto px-6"> + <div className="flex flex-col justify-between h-[300dvh]"> + <JsonToHtmlDL jsonContent={infoDialog.body} /> + </div> + </div> + <DialogFooter className="sm:justify-start gap-2 p-6 pt-0"> + <DialogClose asChild> + <Button> + close + </Button> + </DialogClose> + </DialogFooter> + </DialogContent> + </Dialog > + ); +} + +export default React.memo(InfoBox); \ No newline at end of file diff --git a/src/components/boxes/Infobox.jsx b/src/components/boxes/Infobox.jsx deleted file mode 100755 index 4b259140cb2fdf83064d4ef0b8fb3902ce7b5e74..0000000000000000000000000000000000000000 --- a/src/components/boxes/Infobox.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import React from 'react'; -import { twMerge } from 'tailwind-merge'; - -function Infobox({ children, className, type, ...props }) { - // ################################# - // HOOKS - // ################################# - - // ################################# - // FUNCTIONS - // ################################# - // ### MERGE CLASSNAMES - // static class names every item should have - let staticClassName = 'box-border border border-UhhBlue p-4 my-4 md:inline-block'; - // dynamic class names depending on the type - const typeClassName = () => { - switch (type) { - case 'warning': { - return 'border-orange-300 bg-orange-100/20'; - } - case 'alert': { - return 'border-UhhRed bg-red-100/20'; - } - default: { - return ''; - } - } - }; - // merge static and dynamic class names with className from props - const mergedClassName = twMerge(staticClassName, typeClassName(), className); - - // ################################# - // OUTPUT - // ################################# - return ( - <div> - <div className={mergedClassName}>{children}</div> - </div> - ); -} - -export default React.memo(Infobox); \ No newline at end of file diff --git a/src/components/boxes/JsonToHtmlDL.jsx b/src/components/boxes/JsonToHtmlDL.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ca1a03ea88adb2ac1460ef6d80e475a6d90b27a0 --- /dev/null +++ b/src/components/boxes/JsonToHtmlDL.jsx @@ -0,0 +1,36 @@ +import React from 'react'; + +function JsonToHtmlDL({ jsonContent }) { + // ################################# + // HOOKS + // ################################# + + // ################################# + // FUNCTIONS + // ################################# + const renderDL = (obj) => { + if (obj === null || typeof obj !== 'object') { + return null; + } + + return ( + <dl className='ml-2'> + {Object.entries(obj).map(([key, value]) => ( + <React.Fragment key={key}> + <dt className='text-UhhRed uppercase'>{key}</dt> + <dd className='ml-4 whitespace-pre'> + {typeof value === 'object' && value !== null ? renderDL(value) : (value ? value.toString() : '')} + </dd> + </React.Fragment> + ))} + </dl> + ); + }; + + // ################################# + // OUTPUT + // ################################# + return <div>{renderDL(jsonContent)}</div>; +} + +export default React.memo(JsonToHtmlDL); \ No newline at end of file diff --git a/src/components/form/Select.jsx b/src/components/form/Select.jsx index cd16a7021fe0cc81cc46da0837a06e3fe798c806..3299f0cd8e56216ce93cbbe051794705846b85f0 100755 --- a/src/components/form/Select.jsx +++ b/src/components/form/Select.jsx @@ -15,9 +15,6 @@ function Select({ options, name, title, defaultValue, className, tooltip, type, formState: { errors } } = useFormContext(); - - - // GENERATE RANDOM UUID const id = useId(); diff --git a/src/components/form/Submit.jsx b/src/components/form/Submit.jsx index f244f1bacdfe1de1c19e80f8aa8ddcf1e58ad752..141eab111457581b3f9f9e19d84d7e4f1ac68a8c 100755 --- a/src/components/form/Submit.jsx +++ b/src/components/form/Submit.jsx @@ -1,8 +1,10 @@ +import { cva } from 'class-variance-authority'; import React from 'react'; import { useFormContext } from 'react-hook-form'; import { twMerge } from 'tailwind-merge'; +import { cn } from '../../utils'; -function Submit({ value, type, className, ...props }) { +function Submit({ value, type, className, variant, size, ...props }) { // ################################# // HOOKS // ################################# @@ -24,6 +26,73 @@ function Submit({ value, type, className, ...props }) { } } }; + + const buttonVariants = cva([ + "inline-block", + "w-full", + "mb-4", + "box-border", + "border", + "border-UhhBlue", + "text-center", + "align-middle", + "bg-UhhBlue", + "border-UhhBlue", + "text-UhhWhite", + "font-UhhBC", + "cursor-pointer", + "disabled:cursor-not-allowed", + "disabled:opacity-60", + "lg:min-w-xs", + "lg:w-[calc(1/2*100%-(1*1rem/2))]", + "xl:w-[calc((1/4*100%)-(3*1rem/4))]", + "hover:text-UhhBlue", + "hover:bg-UhhWhite"], + { + variants: { + variant: { + default: ["border-primary", + "bg-primary", + "text-primary-foreground", + "hover:bg-UhhWhite", + "hover:text-primary"], + destructive: + ["border-destructive", + "bg-destructive", + "text-destructive-foreground", + "hover:bg-UhhWhite", + "hover:text-destructive"], + outline: + ["border", + "border-input", + "bg-background", + "hover:bg-accent", + "hover:text-accent-foreground"], + secondary: + ["bg-secondary", + "text-secondary-foreground", + "hover:bg-secondary/80"], + ghost: ["hover:bg-accent", + "hover:text-accent-foreground"], + link: ["text-primary", + "underline-offset-4", + "hover:underline"], + }, + size: { + default: "h-16", + sm: "h-12 w-fit px-4", + lg: "h-16 px-8", + xl: "h-20 px-12", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } + ); + // merge static and dynamic class names with className from props const mergedClassName = twMerge(staticClassName, typeClassName(), className); // ################################# @@ -32,7 +101,7 @@ function Submit({ value, type, className, ...props }) { return ( <> {/* <input type="submit" className={mergedClassName} value={value} disabled={isSubmitting} /> */} - <input type="submit" className={mergedClassName} value={value} disabled={!isValid || isSubmitting} {...props} /> + <input type="submit" className={cn(buttonVariants({ variant, size, className }))} value={value} disabled={!isValid || isSubmitting} {...props} /> </> ); } diff --git a/src/components/table/DataTableViewOptions.jsx b/src/components/table/DataTableViewOptions.jsx new file mode 100644 index 0000000000000000000000000000000000000000..967d5acb4247fd71f1d4bd1e79bf880a7fb4261f --- /dev/null +++ b/src/components/table/DataTableViewOptions.jsx @@ -0,0 +1,61 @@ +import React, { useState } from 'react'; +import { Button } from '../ui/button'; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "/src/components/ui/dropdown-menu"; + +function DataTableViewOptions({ table }) { + // ################################# + // HOOKS + // ################################# + // force rerender to toggle visibility in dropdown + const [rerender, setRerender] = useState(0); + + // ################################# + // FUNCTIONS + // ################################# + // ### GET HIDEABLE COLUMNS + const hideableCols = () => { + return table.getAllColumns().filter(column => typeof column.accessorFn !== "undefined" && column.getCanHide()); + }; + // ################################# + // OUTPUT + // ################################# + return ( + <> + {hideableCols().length > 0 && + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="outline" className="ml-auto"> + Columns + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + {hideableCols() + .map((column) => { + return ( + <DropdownMenuCheckboxItem + key={column.id} + className="capitalize" + checked={column.getIsVisible()} + onCheckedChange={(value) => { + setRerender(rerender + 1); + column.toggleVisibility(!!value); + } + } + > + {column.id} + </DropdownMenuCheckboxItem> + ); + })} + </DropdownMenuContent> + </DropdownMenu> + } + </> + ); +} + +export default React.memo(DataTableViewOptions); \ No newline at end of file diff --git a/src/components/table/Pagination.jsx b/src/components/table/Pagination.jsx new file mode 100644 index 0000000000000000000000000000000000000000..34e757d1dc2d51c87e92eb2e05862186e7d50f06 --- /dev/null +++ b/src/components/table/Pagination.jsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Button } from '../ui/button'; +import { RiSkipLeftLine, RiSkipRightLine } from 'react-icons/ri'; + +function Pagination({ table }) { + // ################################# + // HOOKS + // ################################# + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <div className="flex items-center justify-between"> + {/* ITEM COUNT */} + <span className='text-xs'>{table.getFilteredRowModel().rows.length} Items</span> + {/* PAGE COUNT */} + <span className='text-xs'>Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}</span> + {/* PAGINATION */} + <span className='flex items-center justify-end space-x-2 py-4'> + <Button + type="button" + variant="outline" + size="sm" + onClick={() => table.previousPage()} + disabled={!table.getCanPreviousPage()} + > + <RiSkipLeftLine /> + </Button> + + <Button + type="button" + variant="outline" + size="sm" + onClick={() => table.nextPage()} + disabled={!table.getCanNextPage()} + > + <RiSkipRightLine /> + </Button> + </span> + </div> + ); +} + +export default Pagination; \ No newline at end of file diff --git a/src/components/table/TBody.jsx b/src/components/table/TBody.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f097e921814a00307ee5032836dbf8184aa61523 --- /dev/null +++ b/src/components/table/TBody.jsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { TableBody, TableCell, TableRow } from '../ui/table'; +import { flexRender } from '@tanstack/react-table'; + +function TBody({ table }) { + // ################################# + // HOOKS + // ################################# + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <TableBody> + {table.getRowModel().rows.map(row => { + return ( + <TableRow key={row.id}> + {row.getVisibleCells().map(cell => { + return ( + <TableCell key={cell.id} className={cell.column.columnDef.meta?.cellClassName}> + {flexRender(cell.column.columnDef.cell, cell.getContext())} + </TableCell> + ); + })} + </TableRow> + ); + })} + </TableBody> + ); +} + +export default TBody; \ No newline at end of file diff --git a/src/components/table/THead.jsx b/src/components/table/THead.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e11b01a270e5fad725f2beb64350c55bc8d147ef --- /dev/null +++ b/src/components/table/THead.jsx @@ -0,0 +1,101 @@ +import React, { Fragment } from 'react'; +import { TableHead, TableHeader, TableRow } from '../ui/table'; +import { RiLineHeight, RiSearchLine, RiSortAsc, RiSortDesc } from 'react-icons/ri'; + + + +function THead({ table, setColumnFilters, ...props }) { + // ################################# + // HOOKS + // ################################# + + + // ################################# + // FUNCTIONS + // ################################# + // ### RENDER TITLE CELL + const renderTitleCell = (header) => { + if (header.column.getCanSort()) { + // return title cell with sorting icon + return ( + <TableHead key={header.id} className={`${header.column.columnDef.meta?.headerClassName} cursor-pointer group bg-UhhWhite bg-clip-padding`} onClick={header.column.getToggleSortingHandler()}> + <span className='flex justify-between items-center pointer-events-none'> + {header.column.columnDef.header} + {renderOrderIcon(header.column.getIsSorted())} + </span> + </TableHead>); + } else { + // prevent error on columns without header + const title = (typeof header.column.columnDef.header === 'string') ? header.column.columnDef.header : ''; + return <TableHead key={header.id} className={`${header.column.columnDef.meta?.headerClassName} cursor-pointer group bg-UhhWhite bg-clip-padding`}>{title}</TableHead>; + } + }; + + // ### RENDER ORDER ICON + const renderOrderIcon = (sorted) => { + // check if this is the ordered column + switch (sorted) { + case 'asc': + return <RiSortDesc className='pointer-events-none' />; + case 'desc': + return <RiSortAsc className='pointer-events-none' />; + default: + // if it's not the ordered column, return a different icon + return <span className='text-muted group-hover:text-muted-foreground'> + <RiLineHeight className='pointer-events-none rotate-180' /> + </span>; + } + }; + + + // ### GET FILTERABLE COLUMNS + const filterableCols = () => { + return table.getAllColumns().filter(column => column.getCanFilter() || false); + }; + + // ### RENDER FILTER CELL + const renderFilterCell = (header) => { + if (header.column.columnDef.enableColumnFilter) { + return <TableHead key={header.id}> + <span className='flex items-center'> + <RiSearchLine className='text-UhhBlue' /> + <input type="search" name={header.column.columnDef.accessorKey} placeholder={`filter ${header.column.columnDef.header}`} + className='p-1 w-full outline-none border-none bg-transparent focus:ring-0 font-UhhR text-xs text-UhhRed' onChange={(e) => { onFilterChange(e.target.name, e.target.value); }} /> + </span> + </TableHead>; + } + // if it's not filterable, return an empty cell + return <TableHead key={header.id}></TableHead>; + }; + + // ### CHANGE FILTER + const onFilterChange = (id, value) => setColumnFilters( + prev => prev.filter(f => f.id !== id).concat({ id, value }) + ); + + // ################################# + // OUTPUT + // ################################# + return ( + <TableHeader className='sticky top-0'> + {/* TITLES */} + {table.getHeaderGroups().map(headerGroup => { + return ( + <Fragment key={headerGroup.id}> + <TableRow> + {headerGroup.headers.map(header => { + return renderTitleCell(header); + })} + </TableRow> + {/* FILTERS */} + {filterableCols().length > 0 && <TableRow> + {headerGroup.headers.map((header) => renderFilterCell(header))} + </TableRow>} + </Fragment> + ); + })} + </TableHeader> + ); +} + +export default THead; \ No newline at end of file diff --git a/src/components/table/customTable.jsx b/src/components/table/customTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..07c5742b17494ff8856e15167ad527d6e57bec3f --- /dev/null +++ b/src/components/table/customTable.jsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import { getCoreRowModel, getFilteredRowModel, getPaginationRowModel, getSortedRowModel, useReactTable } from '@tanstack/react-table'; +import { Table } from "/src/components/ui/table"; +import Heading from '../font/Heading'; +import DataTableViewOptions from './DataTableViewOptions'; +import THead from './THead'; +import Pagination from './Pagination'; +import TBody from './TBody'; + + +function CustomTable({ columns = [], data = {}, title = '' }) { + // ################################# + // HOOKS + // ################################# + + // ### INIT FILTERS + const [columnFilters, setColumnFilters] = useState([]); + // ### INIT VISIBILITY + const [columnVisibility, setColumnVisibility] = useState({}); + // ### INIT TABLE + const table = useReactTable({ + data, + columns, + state: { + columnFilters, + columnVisibility + }, + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getCoreRowModel: getCoreRowModel(), + onColumnVisibilityChange: setColumnVisibility, + // define custom filter functions + filterFns: { + regex: (rows, columnIds, filterValue) => { + try { + console.log("🚀 ~ CustomTable ~ rows.getValue(columnIds):", rows.getValue(columnIds)); + + const regex = new RegExp(filterValue, 'i'); + return rows.getValue(columnIds).toString().match(regex) ? true : false; + } catch (error) { + console.error(error); + return false; + } + } + } + }); + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {/* title line incl. */} + <Heading level="4" className='flex justify between'> + {title} + {/* HIDE COLUMNS */} + <DataTableViewOptions table={table} /> + </Heading> + <Table className='w-full border-collapse table-auto border border-UhhLightGrey'> + {/* HEADER */} + <THead table={table} setColumnFilters={setColumnFilters} /> + {/* BODY */} + {table.getRowModel().rows.length > 0 && <TBody table={table} />} + </Table> + {table.getRowModel().rows.length === 0 && <div className='text-center'>No matching data found</div>} + + {/* FOOTER */} + {table.getRowModel().rows.length > 0 && <Pagination table={table} />} + + </> + ); +}; + +export default React.memo(CustomTable); \ No newline at end of file diff --git a/src/components/ui/button.jsx b/src/components/ui/button.jsx new file mode 100644 index 0000000000000000000000000000000000000000..46f3ccb4c0749027c6ca96dd0c26e0693e930cae --- /dev/null +++ b/src/components/ui/button.jsx @@ -0,0 +1,47 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva } from "class-variance-authority"; + +import { cn } from "/src/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center text-sm font-UhhBC ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 box-border border", + { + variants: { + variant: { + default: "border-primary bg-primary text-primary-foreground hover:bg-UhhWhite hover:text-primary", + destructive: + "border-destructive bg-destructive text-destructive-foreground hover:bg-UhhWhite hover:text-destructive", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 px-3", + lg: "h-11 px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + (<Comp + className={cn(buttonVariants({ variant, size, className }))} + ref={ref} + {...props} />) + ); +}); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/src/components/ui/dialog.jsx b/src/components/ui/dialog.jsx new file mode 100644 index 0000000000000000000000000000000000000000..838e77053c73f515ae4b1738a4937b615612d067 --- /dev/null +++ b/src/components/ui/dialog.jsx @@ -0,0 +1,94 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "/src/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => ( + <DialogPrimitive.Overlay + ref={ref} + className={cn( + "fixed inset-0 z-50 bg-background/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", + className + )} + {...props} /> +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => ( + <DialogPortal> + <DialogOverlay /> + <DialogPrimitive.Content + ref={ref} + className={cn( + "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg md:w-full", + className + )} + {...props}> + {children} + <DialogPrimitive.Close + className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"> + <X className="h-4 w-4" /> + <span className="sr-only">Close</span> + </DialogPrimitive.Close> + </DialogPrimitive.Content> + </DialogPortal> +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}) => ( + <div + className={cn("flex flex-col space-y-1.5 text-center sm:text-left", className)} + {...props} /> +); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}) => ( + <div + className={cn("flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2", className)} + {...props} /> +); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef(({ className, ...props }, ref) => ( + <DialogPrimitive.Title + ref={ref} + className={cn("text-lg font-semibold leading-none tracking-tight", className)} + {...props} /> +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef(({ className, ...props }, ref) => ( + <DialogPrimitive.Description + ref={ref} + className={cn("text-sm text-muted-foreground", className)} + {...props} /> +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +}; diff --git a/src/components/ui/dropdown-menu.jsx b/src/components/ui/dropdown-menu.jsx new file mode 100644 index 0000000000000000000000000000000000000000..790db69f9a216f945b41910daf6a8ea6a2309d4e --- /dev/null +++ b/src/components/ui/dropdown-menu.jsx @@ -0,0 +1,155 @@ +import * as React from "react"; +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; +import { Check, ChevronRight, Circle } from "lucide-react"; + +import { cn } from "/src/utils"; + +const DropdownMenu = DropdownMenuPrimitive.Root; + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; + +const DropdownMenuGroup = DropdownMenuPrimitive.Group; + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal; + +const DropdownMenuSub = DropdownMenuPrimitive.Sub; + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; + +const DropdownMenuSubTrigger = React.forwardRef(({ className, inset, children, ...props }, ref) => ( + <DropdownMenuPrimitive.SubTrigger + ref={ref} + className={cn( + "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent", + inset && "pl-8", + className + )} + {...props}> + {children} + <ChevronRight className="ml-auto h-4 w-4" /> + </DropdownMenuPrimitive.SubTrigger> +)); +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName; + +const DropdownMenuSubContent = React.forwardRef(({ className, ...props }, ref) => ( + <DropdownMenuPrimitive.SubContent + ref={ref} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} /> +)); +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName; + +const DropdownMenuContent = React.forwardRef(({ className, sideOffset = 4, ...props }, ref) => ( + <DropdownMenuPrimitive.Portal> + <DropdownMenuPrimitive.Content + ref={ref} + sideOffset={sideOffset} + className={cn( + "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} /> + </DropdownMenuPrimitive.Portal> +)); +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; + +const DropdownMenuItem = React.forwardRef(({ className, inset, ...props }, ref) => ( + <DropdownMenuPrimitive.Item + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + inset && "pl-8", + className + )} + {...props} /> +)); +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; + +const DropdownMenuCheckboxItem = React.forwardRef(({ className, children, checked, ...props }, ref) => ( + <DropdownMenuPrimitive.CheckboxItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className + )} + checked={checked} + {...props}> + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Check className="h-4 w-4" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.CheckboxItem> +)); +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName; + +const DropdownMenuRadioItem = React.forwardRef(({ className, children, ...props }, ref) => ( + <DropdownMenuPrimitive.RadioItem + ref={ref} + className={cn( + "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50", + className + )} + {...props}> + <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> + <DropdownMenuPrimitive.ItemIndicator> + <Circle className="h-2 w-2 fill-current" /> + </DropdownMenuPrimitive.ItemIndicator> + </span> + {children} + </DropdownMenuPrimitive.RadioItem> +)); +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; + +const DropdownMenuLabel = React.forwardRef(({ className, inset, ...props }, ref) => ( + <DropdownMenuPrimitive.Label + ref={ref} + className={cn("px-2 py-1.5 text-sm font-semibold", inset && "pl-8", className)} + {...props} /> +)); +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; + +const DropdownMenuSeparator = React.forwardRef(({ className, ...props }, ref) => ( + <DropdownMenuPrimitive.Separator + ref={ref} + className={cn("-mx-1 my-1 h-px bg-muted", className)} + {...props} /> +)); +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; + +const DropdownMenuShortcut = ({ + className, + ...props +}) => { + return ( + (<span + className={cn("ml-auto text-xs tracking-widest opacity-60", className)} + {...props} />) + ); +}; +DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +}; diff --git a/src/components/ui/table.jsx b/src/components/ui/table.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9fdceb5596ba762a1a5594e8b88cb2193907d764 --- /dev/null +++ b/src/components/ui/table.jsx @@ -0,0 +1,83 @@ +import * as React from "react"; + +import { cn } from "/src/utils"; + +const Table = React.forwardRef(({ className, ...props }, ref) => ( + // <div className="relative w-full overflow-auto"> + <table + ref={ref} + className={cn("w-full caption-bottom border-separate border-spacing-0 table-auto", className)} + {...props} /> + // </div> +)); +Table.displayName = "Table"; + +const TableHeader = React.forwardRef(({ className, ...props }, ref) => ( + <thead ref={ref} className={cn("[&_tr]:border-b", className)} {...props} /> +)); +TableHeader.displayName = "TableHeader"; + +const TableBody = React.forwardRef(({ className, ...props }, ref) => ( + <tbody + ref={ref} + className={cn("[&_tr:last-child]:border-0", className)} + {...props} /> +)); +TableBody.displayName = "TableBody"; + +const TableFooter = React.forwardRef(({ className, ...props }, ref) => ( + <tfoot + ref={ref} + className={cn("bg-primary font-medium text-primary-foreground", className)} + {...props} /> +)); +TableFooter.displayName = "TableFooter"; + +const TableRow = React.forwardRef(({ className, ...props }, ref) => ( + <tr + ref={ref} + className={cn( + "border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted", + className + )} + {...props} /> +)); +TableRow.displayName = "TableRow"; + +const TableHead = React.forwardRef(({ className, ...props }, ref) => ( + <th + ref={ref} + className={cn( + "border border-UhhLightGray font-UhhSLC px-1 text-left [&:has([role=checkbox])]:pr-0", + className + )} + {...props} /> +)); +TableHead.displayName = "TableHead"; + +const TableCell = React.forwardRef(({ className, ...props }, ref) => ( + <td + ref={ref} + className={cn("border border-UhhLightGray p-1 align-middle [&:has([role=checkbox])]:pr-0", className)} + {...props} /> +)); +TableCell.displayName = "TableCell"; + +const TableCaption = React.forwardRef(({ className, ...props }, ref) => ( + <caption + ref={ref} + className={cn("mt-4 text-sm text-muted-foreground", className)} + {...props} /> +)); +TableCaption.displayName = "TableCaption"; + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +}; diff --git a/src/pages/Config/AI/Models.jsx b/src/pages/Config/AI/Models.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3a36ab592b2f274b911555cf8c0ab2eeb3b0f1e9 --- /dev/null +++ b/src/pages/Config/AI/Models.jsx @@ -0,0 +1,169 @@ +import React, { useState } from 'react'; +import { useAuth } from '/src/contexts/Auth/AuthState'; +import api from '/src/utils/AxiosConfig'; +import ConfirmBox from '/src/components/boxes/ConfirmBox'; +import InfoBox from '/src/components/boxes/InfoBox'; +import CustomTable from '/src/components/table/customTable'; +import { mergeBackendValidation, setFlashMsg } from '/src/utils/ErrorHandling'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "/src/components/ui/dropdown-menu"; +import { Button } from '/src/components/ui/button'; +import { RiDeleteBinLine, RiFileInfoLine, RiMoreLine, RiRefreshLine } from 'react-icons/ri'; + + +function Models({ data, setData }) { + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { currentUser } = useAuth(); + // ### CONFIRM DIALOG + const [confirmDialog, setConfirmDialog] = useState({ open: false, item: {} }); + // ### INFO DIALOG + const [infoDialog, setInfoDialog] = useState({ open: false, title: '', description: '', body: '', item: {} }); + + // ### HANDLE DELETE ITEM + const handleDelete = async (model) => { + try { + // remove item from backend + const result = await api.delete('/ai/models', { data: { model } }); + // remove item from current display + const filteredData = data.filter(item => item.name !== model); + setData(filteredData); + // show message from backend + setFlashMsg(result?.data?.message); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + + // ### HANDLE UPDATE ITEM + const handleUpdate = async (model) => { + try { + // install item in backend + const result = await api.put('/ai/models', { model }); + // show message from backend + setFlashMsg(result?.data?.message); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + + // ### HANDLE ITEM DETAILS + const handleDetails = async (model) => { + try { + // install item in backend + const result = await api.post('/ai/model', { model }); + // show message from backend + setInfoDialog({ open: true, title: model, description: 'model details', body: result?.data }); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + + // ################################# + // FUNCTIONS + // ################################# + // ### DEFINE TABLE ROWS + const columns = [ + { + accessorKey: 'name', + header: 'name', + enableColumnFilter: true, + enableHiding: false, + filterFn: 'regex', + // cell: (props) => <span>{props.getValue()}</span> + cell: (props) => { + return (props.row.original.name); + } + + }, { + accessorKey: 'size', + header: 'size in bytes', + enableColumnFilter: true, + enableHiding: true, + filterFn: 'includesString', + cell: (props) => <span>{Number(props.getValue())}</span> + // cell: (props) => { + // return (Number(props.row.original.size) / 1024 / 1024 / 1024).toFixed(2).toString(); + // } + }, { + id: 'details.parameter_size', + accessorKey: 'details.parameter_size', + header: 'parameters', + enableColumnFilter: true, + enableHiding: true, + filterFn: 'regex', + cell: (props) => <span>{props.getValue()}</span> + // cell: (props) => { + // return (props.row.original.details.parameter_size); + // } + + }, { + id: "actions", + enableHiding: false, + meta: { + cellClassName: 'text-center w-16', + }, + cell: ({ row }) => { + const item = row.original; + return ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button title="open menu"> + <span className="sr-only">Open menu</span> + <RiMoreLine /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuLabel>Actions</DropdownMenuLabel> + <DropdownMenuItem className='flex items-center space-x-2 w-full' onClick={() => handleDetails(item.name)}> + <span><RiFileInfoLine /></span> + <span>show details</span> + </DropdownMenuItem> + {currentUser.role > 2 ? + <> + <DropdownMenuItem className='cursor-pointer flex items-center space-x-2 w-full' onClick={() => handleUpdate(item.name)}> + <span><RiRefreshLine /></span> + <span>update model</span> + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem className='cursor-pointer text-UhhRed hover:text-UhhWhite hover:bg-UhhRed' onClick={() => setConfirmDialog({ open: true, idToDelete: item.name, displayName: item.name })}> + <span className='flex items-center space-x-2 w-full'> + <span><RiDeleteBinLine /></span> + <span>delete model</span> + </span> + </DropdownMenuItem> + </> + : null} + </DropdownMenuContent> + </DropdownMenu> + ); + }, + } + ]; + // ################################# + // OUTPUT + // ################################# + return ( + <div> + {/* table */} + <CustomTable columns={columns} data={data} title='installed models' /> + + {/* confirmDialog */} + <ConfirmBox confirmDialog={confirmDialog} closeDialog={() => setConfirmDialog({ ...confirmDialog, open: false })} handleProceed={() => { handleDelete(confirmDialog.idToDelete); }} /> + + {/* infoDialog */} + <InfoBox infoDialog={infoDialog} closeDialog={() => setInfoDialog({ ...infoDialog, open: false })} /> + + </div> + ); +} + +export default React.memo(Models); \ No newline at end of file diff --git a/src/pages/Config/AI/NewModel.jsx b/src/pages/Config/AI/NewModel.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3e41ab24e0c61d450bd631d7a97b7aca943b6676 --- /dev/null +++ b/src/pages/Config/AI/NewModel.jsx @@ -0,0 +1,67 @@ +import React, { useEffect, useState } from 'react'; +import { useAuth } from '/src/contexts/Auth/AuthState'; +import api from '/src/utils/AxiosConfig'; +import Heading from '/src/components/font/Heading'; +import { mergeBackendValidation, setFlashMsg } from '/src/utils/ErrorHandling'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { FormProvider, useForm, useFormContext } from 'react-hook-form'; +import Input from '/src/components/form/Input'; +import Submit from '/src/components/form/Submit'; +import { Link } from 'react-router-dom'; + +function NewModel({ data, setData }) { + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { currentUser } = useAuth(); + + // ### SCHEMA + const schema = z.object({ + model: z.string().min(1), + }); + + // ### FORM HOOKS + const methods = useForm({ + resolver: zodResolver(schema), + mode: 'onSubmit' + }); + + // ################################# + // FUNCTIONS + // ################################# + // ### HANDLE INSTALL ITEM + const handleInstall = async (inputs) => { + try { + // install item in backend + const result = await api.put('/ai/models', { model: inputs.model }); + // add new model to the list + setData([...data, result.data.model[0]]); + // show message from backend + setFlashMsg(result?.data?.message); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {(currentUser?.role >= 2) ? + <FormProvider {...methods} > + <Heading level="4">install new model</Heading> + <form onSubmit={methods.handleSubmit(handleInstall)} className='md:w-1/3'> + <Input name='model' type='text' title='Model Name' className='h-16' required={true} tooltip={<Link to='https://ollama.com/library' target='_blank' rel='noopener noreferrer'>Ollama Library</Link>} /> + <Submit size='sm' value={methods.formState.isSubmitting ? 'installing...' : 'install model'} /> + </form> + </FormProvider> + : null} + + </> + ); +} + +export default React.memo(NewModel); \ No newline at end of file diff --git a/src/pages/Config/AI/Status.jsx b/src/pages/Config/AI/Status.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c6f52227e10b102df0d4e146923064daca7c8150 --- /dev/null +++ b/src/pages/Config/AI/Status.jsx @@ -0,0 +1,54 @@ +import React, { useEffect, useState } from 'react'; +import { mergeBackendValidation } from '../../../utils/ErrorHandling'; +import api from '../../../utils/AxiosConfig'; +import { RiWifiFill, RiWifiOffFill } from "react-icons/ri"; +import Heading from '../../../components/font/Heading'; + +function AIStatus() { + // ################################# + // HOOKS + // ################################# + // ### SET STATUS + const [status, setStatus] = useState({}); + + // ### FETCH STATUS + useEffect(() => { + // ### on run exec this code + const controller = new AbortController(); + const getStatus = async () => { + try { + // fetch all items and store them in state + const result = await api.get('/ai/status', { + signal: controller.signal + }); + + setStatus(result.data.running); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + getStatus(); + // ### return will be executed on unmounting this component + return () => { + // on unmount abort request + controller.abort(); + }; + // ### opt. 2. argument: when to run this hook + }, []); + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <Heading level="4">status: + {status ? + <RiWifiFill className='ml-4 text-UhhBlue' title='AI backend reachable' /> + : <RiWifiOffFill className='ml-4 text-UhhRed' title='AI backend offline' />} + </Heading> + ); +} + +export default React.memo(AIStatus); \ No newline at end of file diff --git a/src/pages/Config/AIModels.jsx b/src/pages/Config/AIModels.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7eb9bf1408c6bec1e25b7118da890739ba2691f3 --- /dev/null +++ b/src/pages/Config/AIModels.jsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState } from 'react'; +import api from '../../utils/AxiosConfig'; +import { mergeBackendValidation } from '../../utils/ErrorHandling'; +import NewModel from './AI/NewModel'; +import Models from './AI/Models'; +import AIStatus from './AI/Status'; + +function AIModels() { + // ################################# + // HOOKS + // ################################# + // ### SET TABLE DATA + const [data, setData] = useState({}); + + // ### FETCH MODELS + useEffect(() => { + // ### on run exec this code + const controller = new AbortController(); + const getDocument = async () => { + try { + // fetch all items and store them in state + const items = await api.post('/ai/models', { filter: '' }, { + signal: controller.signal + }); + + setData(items.data.models); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + getDocument(); + // ### return will be executed on unmounting this component + return () => { + // on unmount abort request + controller.abort(); + }; + // ### opt. 2. argument: when to run this hook + }, []); + + + + // ################################# + // FUNCTIONS + // ################################# + + + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {/* AI */} + <section> + {/* <!-- header --> */} + <div className="group sticky top-0 bg-UhhWhite z-10 mb-4 font-UhhBC text-2xl"> + {/* <!-- number --> */} + <span className="text-UhhRed">01 </span> + {/* <!-- title --> */} + AI + {/* <!-- line --> */} + <div className="absolute w-[50vw] right-[50%] h-1 bg-UhhRed"></div> + </div> + + <div className='max-h-full flex flex-col'> + <div> + <AIStatus /> + </div> + <div> + <NewModel data={data} setData={setData} /> + </div> + <div className='overflow-y-auto'> + <Models data={data} setData={setData} /> + </div> + </div> + + + + </section> + </> + ); +} + +export default React.memo(AIModels); \ No newline at end of file diff --git a/src/pages/Config/Config.jsx b/src/pages/Config/Config.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b3bc0e0d99820eda692b45c018036571fa2a2ae1 --- /dev/null +++ b/src/pages/Config/Config.jsx @@ -0,0 +1,34 @@ +import React, { useEffect, useState } from 'react'; +import { Helmet } from 'react-helmet-async'; +import { useAuth } from '../../contexts/Auth/AuthState'; +import AIModels from './AIModels'; +import Embeddings from './Embeddings'; + + +function Config() { + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { currentUser } = useAuth(); + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <> + {/* render page title */} + <Helmet><title>[{import.meta.env.VITE_APP_NAME}] Config</title></Helmet> + {/* AI */} + <AIModels /> + {/* RAG */} + <Embeddings /> + </> + ); +} + +export default React.memo(Config); \ No newline at end of file diff --git a/src/pages/Config/Embeddings.jsx b/src/pages/Config/Embeddings.jsx new file mode 100644 index 0000000000000000000000000000000000000000..80bc05b0f77ba92c1247daae241b86d1e5a9051f --- /dev/null +++ b/src/pages/Config/Embeddings.jsx @@ -0,0 +1,76 @@ +import React, { useEffect, useState } from 'react'; +import Status from './Embeddings/Status'; +import { mergeBackendValidation } from '../../utils/ErrorHandling'; +import api from '../../utils/AxiosConfig'; +import Update from './Embeddings/Update'; +import Delete from './Embeddings/Delete'; + +function Embeddings() { + // ################################# + // HOOKS + // ################################# + // ### SET DATA + const [status, setStatus] = useState({}); + + // ### FETCH MODELS + useEffect(() => { + // ### on run exec this code + const controller = new AbortController(); + const getStatus = async () => { + try { + // fetch all items and store them in state + const result = await api.get('/embeddings', { + signal: controller.signal + }); + setStatus(result.data); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + getStatus(); + // ### return will be executed on unmounting this component + return () => { + // on unmount abort request + controller.abort(); + }; + // ### opt. 2. argument: when to run this hook + }, []); + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <section> + {/* <!-- header --> */} + <div className="group sticky top-0 bg-UhhWhite z-10 mb-4 font-UhhBC text-2xl"> + {/* <!-- number --> */} + <span className="text-UhhRed">02 </span> + {/* <!-- title --> */} + Embeddings + {/* <!-- line --> */} + <div className="absolute w-[50vw] right-[50%] h-1 bg-UhhRed"></div> + </div> + + <div className='max-h-full flex flex-col'> + <div> + {/* rag status */} + <Status status={status} /> + </div> + <div> + {/* update embeddings */} + <Update setStatus={setStatus} /> + </div> + <div> + {/* delete embeddings */} + <Delete setStatus={setStatus} /> + </div> + </div> + </section> + ); +} + +export default React.memo(Embeddings); \ No newline at end of file diff --git a/src/pages/Config/Embeddings/Delete.jsx b/src/pages/Config/Embeddings/Delete.jsx new file mode 100644 index 0000000000000000000000000000000000000000..928d9cfbcdfdaac984f0e97311129d24fd3b9b1f --- /dev/null +++ b/src/pages/Config/Embeddings/Delete.jsx @@ -0,0 +1,75 @@ +import React, { useState } from 'react'; +import { useAuth } from '../../../contexts/Auth/AuthState'; +import { FormProvider, useForm } from 'react-hook-form'; +import Heading from '../../../components/font/Heading'; +import Input from '../../../components/form/Input'; +import Submit from '../../../components/form/Submit'; +import ConfirmBox from '../../../components/boxes/ConfirmBox'; +import { mergeBackendValidation, setFlashMsg } from '../../../utils/ErrorHandling'; +import api from '../../../utils/AxiosConfig'; + +function Delete({ setStatus }) { + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { currentUser } = useAuth(); + // ### FORM HOOKS + const methods = useForm(); + // ### CONFIRM DIALOG + const [confirmDialog, setConfirmDialog] = useState({ open: false, item: {} }); + + // ################################# + // FUNCTIONS + // ################################# + // security + const handleConfirm = () => { + setConfirmDialog({ open: true, displayName: 'Vector Database' }); + }; + + // ### HANDLE INSTALL ITEM + const handleDelete = async () => { + try { + // install item in backend + const result = await api.delete('/embeddings'); + + console.log("🚀 ~ handleDelete ~ result:", result); + + // renew status + setStatus(result.data); + // show message from backend + setFlashMsg(result?.data?.message); + } catch (error) { + + console.log("🚀 ~ handleDelete ~ error:", error); + + + mergeBackendValidation(error.response.status, error.response.data); + } + }; + + // ################################# + // OUTPUT + // ################################# + return ( + <div> + {(currentUser?.role >= 2) ? + <> + <FormProvider {...methods} > + <Heading level="4">Delete Embedding Collection</Heading> + <form onSubmit={methods.handleSubmit(handleConfirm)} className='md:w-1/3'> + <Submit variant='destructive' size='sm' value={methods.formState.isSubmitting ? 'deleting...' : 'delete'} /> + </form> + </FormProvider> + + + <ConfirmBox confirmDialog={confirmDialog} closeDialog={() => setConfirmDialog({ ...confirmDialog, open: false })} handleProceed={() => { handleDelete(confirmDialog.idToDelete); }} /> + + </> + + : null} + </div> + ); +} + +export default React.memo(Delete); \ No newline at end of file diff --git a/src/pages/Config/Embeddings/Status.jsx b/src/pages/Config/Embeddings/Status.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f54a2d95b27b3bcb13b5ddf3f17105cdf08b69ba --- /dev/null +++ b/src/pages/Config/Embeddings/Status.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import JsonToHtmlDL from '../../../components/boxes/JsonToHtmlDL'; +import Heading from '../../../components/font/Heading'; + +function Status({ status }) { + // ################################# + // HOOKS + // ################################# + + // ################################# + // FUNCTIONS + // ################################# + + // ################################# + // OUTPUT + // ################################# + return ( + <div> + <Heading level="4">Status</Heading> + <JsonToHtmlDL jsonContent={status} /> + </div> + ); +} + +export default React.memo(Status); \ No newline at end of file diff --git a/src/pages/Config/Embeddings/Update.jsx b/src/pages/Config/Embeddings/Update.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b2a2564e3eb8626ce6117e158706e029c8fdb18e --- /dev/null +++ b/src/pages/Config/Embeddings/Update.jsx @@ -0,0 +1,69 @@ +import React, { useState } from 'react'; +import { Button } from '../../../components/ui/button'; +import Heading from '../../../components/font/Heading'; +import Tooltip from '../../../components/boxes/Tooltip'; +import api from '../../../utils/AxiosConfig'; +import { useAuth } from '../../../contexts/Auth/AuthState'; +import { FormProvider, useForm } from 'react-hook-form'; +import Submit from '../../../components/form/Submit'; +import { mergeBackendValidation, setFlashMsg } from '../../../utils/ErrorHandling'; +import JsonToHtmlDL from '../../../components/boxes/JsonToHtmlDL'; + +function Update({ setStatus }) { + // ################################# + // HOOKS + // ################################# + // ### CONNECT AUTH CONTEXT + const { currentUser } = useAuth(); + + // ### SET DATA + const [data, setData] = useState({}); + + // ### FORM HOOKS + const methods = useForm(); + // ################################# + // FUNCTIONS + // ################################# + // ### UPDATE EMBEDDINGS + const handleUpdate = async () => { + try { + // update + const update = await api.patch('/embeddings'); + // renew status + setData(update.data); + + // renew status + const status = await api.get('/embeddings'); + setStatus(status.data); + + + // show message from backend + setFlashMsg(update.data?.message); + } catch (error) { + mergeBackendValidation(error.response.status, error.response.data); + } + }; + // ################################# + // OUTPUT + // ################################# + return ( + <> + {(currentUser?.role >= 2) ? + <FormProvider {...methods}> + <Heading level="4">Update Embeddings + <Tooltip><p className='text-base'>based on local RAG Files</p></Tooltip> + </Heading> + <form onSubmit={methods.handleSubmit(handleUpdate)} className='md:w-1/3'> + <Submit size='sm' value={methods.formState.isSubmitting ? 'updating...' : 'update'} /> + </form> + <details className='py-4 border-b border-grey-lighter'> + <summary>Update Result</summary> + <JsonToHtmlDL jsonContent={data} /> + </details> + </FormProvider> + : null} + </> + ); +} + +export default React.memo(Update); \ No newline at end of file diff --git a/src/routes/Sitemap.jsx b/src/routes/Sitemap.jsx index 17567fff259867dc23a2aff8433d9e7efd6e3242..72b095386acd7ccf140d2cb71aa853cd9b09faf4 100644 --- a/src/routes/Sitemap.jsx +++ b/src/routes/Sitemap.jsx @@ -27,6 +27,17 @@ export const sitemap = [{ ] }, + // ADMIN + { + title: 'Config', + path: '/config', + handle: { crumb: () => <Link to="/config">Config</Link> }, + children: [ + { index: true, element: loadComponent('Config/Config') }, + { title: 'Chat', path: ':id', element: loadComponent('Config/Onboarding') } + + ] + }, // LOGOUT { title: 'Logout',