From f4c1d03dc821e6b719801ff5a232794e674d35ea Mon Sep 17 00:00:00 2001 From: susie-laptop Date: Tue, 15 Apr 2025 21:51:27 -0400 Subject: [PATCH] Initial commit --- .env | 4 + .env.development | 4 + .env.production | 4 + .gitattributes | 2 + eslint.config.js | 38 + index.html | 13 + package-lock.json | 6396 +++++++++++++++++ package.json | 42 + src/App.jsx | 61 + src/assets/react.svg | 1 + src/components/AccessRequestModal.jsx | 232 + src/components/ApiModeSwitch.jsx | 83 + src/components/CodeBlock.jsx | 41 + src/components/CreateKnowledgeBaseModal.jsx | 287 + src/components/ErrorBoundary.jsx | 47 + src/components/Loading.jsx | 11 + src/components/NotificationCenter.jsx | 310 + src/components/NotificationSnackbar.jsx | 22 + src/components/Pagination.jsx | 59 + src/components/SafeMarkdown.jsx | 50 + src/components/SearchBar.jsx | 215 + src/components/Snackbar.jsx | 41 + src/components/SvgIcon.jsx | 46 + src/components/UserSettingsModal.jsx | 268 + src/icons/icons.js | 125 + src/layouts/HeaderWithNav.jsx | 168 + src/layouts/Mainlayout.jsx | 13 + src/main.jsx | 22 + src/pages/Chat/Chat.jsx | 179 + src/pages/Chat/ChatSidebar.jsx | 134 + src/pages/Chat/ChatWindow.jsx | 403 ++ src/pages/Chat/NewChat.jsx | 256 + src/pages/KnowledgeBase/Detail/DatasetTab.jsx | 386 + .../Detail/KnowledgeBaseDetail.jsx | 119 + .../KnowledgeBase/Detail/SettingsTab.jsx | 351 + .../Detail/components/Breadcrumb.jsx | 34 + .../Detail/components/DeleteConfirmModal.jsx | 70 + .../Detail/components/DocumentList.jsx | 222 + .../components/DocumentPreviewModal.jsx | 132 + .../Detail/components/FileUploadModal.jsx | 293 + .../Detail/components/KnowledgeBaseForm.jsx | 283 + .../components/UserPermissionsManager.jsx | 789 ++ src/pages/KnowledgeBase/KnowledgeBase.css | 53 + src/pages/KnowledgeBase/KnowledgeBase.jsx | 538 ++ .../components/KnowledgeBaseList.jsx | 41 + .../components/KnowledgeCard.jsx | 121 + src/pages/Permissions/Permissions.css | 39 + src/pages/Permissions/PermissionsPage.jsx | 24 + .../components/PendingRequests.css | 251 + .../components/PendingRequests.jsx | 470 ++ .../components/RequestDetailSlideOver.jsx | 202 + .../components/UserPermissionDetails.jsx | 300 + .../components/UserPermissions.css | 210 + .../components/UserPermissions.jsx | 373 + src/pages/auth/Login.jsx | 120 + src/pages/auth/Signup.jsx | 274 + src/router/protectedRoute.jsx | 12 + src/router/router.jsx | 92 + src/services/api.js | 321 + src/services/mockApi.js | 1131 +++ src/services/permissionService.js | 56 + src/services/userServices.js | 13 + src/services/websocket.js | 245 + src/store/auth/auth.slice.js | 72 + src/store/auth/auth.thunk.js | 139 + src/store/chat/chat.messages.thunks.js | 45 + src/store/chat/chat.slice.js | 354 + src/store/chat/chat.thunks.js | 491 ++ .../knowledgeBase/knowledgeBase.slice.js | 269 + .../knowledgeBase/knowledgeBase.thunks.js | 333 + src/store/notification.slice.js | 14 + .../notificationCenter.slice.js | 123 + src/store/permissions/permissions.slice.js | 159 + src/store/permissions/permissions.thunks.js | 167 + src/store/store.js | 41 + src/styles/base.scss | 12 + src/styles/style.scss | 446 ++ src/utils/dateUtils.js | 48 + vite.config.js | 25 + 79 files changed, 19880 insertions(+) create mode 100644 .env create mode 100644 .env.development create mode 100644 .env.production create mode 100644 .gitattributes create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/App.jsx create mode 100644 src/assets/react.svg create mode 100644 src/components/AccessRequestModal.jsx create mode 100644 src/components/ApiModeSwitch.jsx create mode 100644 src/components/CodeBlock.jsx create mode 100644 src/components/CreateKnowledgeBaseModal.jsx create mode 100644 src/components/ErrorBoundary.jsx create mode 100644 src/components/Loading.jsx create mode 100644 src/components/NotificationCenter.jsx create mode 100644 src/components/NotificationSnackbar.jsx create mode 100644 src/components/Pagination.jsx create mode 100644 src/components/SafeMarkdown.jsx create mode 100644 src/components/SearchBar.jsx create mode 100644 src/components/Snackbar.jsx create mode 100644 src/components/SvgIcon.jsx create mode 100644 src/components/UserSettingsModal.jsx create mode 100644 src/icons/icons.js create mode 100644 src/layouts/HeaderWithNav.jsx create mode 100644 src/layouts/Mainlayout.jsx create mode 100644 src/main.jsx create mode 100644 src/pages/Chat/Chat.jsx create mode 100644 src/pages/Chat/ChatSidebar.jsx create mode 100644 src/pages/Chat/ChatWindow.jsx create mode 100644 src/pages/Chat/NewChat.jsx create mode 100644 src/pages/KnowledgeBase/Detail/DatasetTab.jsx create mode 100644 src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx create mode 100644 src/pages/KnowledgeBase/Detail/SettingsTab.jsx create mode 100644 src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx create mode 100644 src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx create mode 100644 src/pages/KnowledgeBase/Detail/components/DocumentList.jsx create mode 100644 src/pages/KnowledgeBase/Detail/components/DocumentPreviewModal.jsx create mode 100644 src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx create mode 100644 src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx create mode 100644 src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx create mode 100644 src/pages/KnowledgeBase/KnowledgeBase.css create mode 100644 src/pages/KnowledgeBase/KnowledgeBase.jsx create mode 100644 src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx create mode 100644 src/pages/KnowledgeBase/components/KnowledgeCard.jsx create mode 100644 src/pages/Permissions/Permissions.css create mode 100644 src/pages/Permissions/PermissionsPage.jsx create mode 100644 src/pages/Permissions/components/PendingRequests.css create mode 100644 src/pages/Permissions/components/PendingRequests.jsx create mode 100644 src/pages/Permissions/components/RequestDetailSlideOver.jsx create mode 100644 src/pages/Permissions/components/UserPermissionDetails.jsx create mode 100644 src/pages/Permissions/components/UserPermissions.css create mode 100644 src/pages/Permissions/components/UserPermissions.jsx create mode 100644 src/pages/auth/Login.jsx create mode 100644 src/pages/auth/Signup.jsx create mode 100644 src/router/protectedRoute.jsx create mode 100644 src/router/router.jsx create mode 100644 src/services/api.js create mode 100644 src/services/mockApi.js create mode 100644 src/services/permissionService.js create mode 100644 src/services/userServices.js create mode 100644 src/services/websocket.js create mode 100644 src/store/auth/auth.slice.js create mode 100644 src/store/auth/auth.thunk.js create mode 100644 src/store/chat/chat.messages.thunks.js create mode 100644 src/store/chat/chat.slice.js create mode 100644 src/store/chat/chat.thunks.js create mode 100644 src/store/knowledgeBase/knowledgeBase.slice.js create mode 100644 src/store/knowledgeBase/knowledgeBase.thunks.js create mode 100644 src/store/notification.slice.js create mode 100644 src/store/notificationCenter/notificationCenter.slice.js create mode 100644 src/store/permissions/permissions.slice.js create mode 100644 src/store/permissions/permissions.thunks.js create mode 100644 src/store/store.js create mode 100644 src/styles/base.scss create mode 100644 src/styles/style.scss create mode 100644 src/utils/dateUtils.js create mode 100644 vite.config.js diff --git a/.env b/.env new file mode 100644 index 0000000..ee6b0ee --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +VITE_PORT = 8080 +VITE_PROD = false +VITE_API_URL = "http://121.4.99.91:8008" +VITE_SECRETKEY = "ooin-knowledge-base-key" \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..1dd297e --- /dev/null +++ b/.env.development @@ -0,0 +1,4 @@ +VITE_PORT = 8080 +VITE_PROD = false +VITE_API_URL = "http://121.4.99.91:8008" +VITE_SECRETKEY = "ooin-knowledge-base-key" diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..9a90567 --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +VITE_PORT = 8080 +VITE_PROD = true +VITE_API_URL = "http://121.4.99.91:8008" +VITE_SECRETKEY = "ooin-knowledge-base-key" \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..238d2e4 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,38 @@ +import js from '@eslint/js' +import globals from 'globals' +import react from 'eslint-plugin-react' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' + +export default [ + { ignores: ['dist'] }, + { + files: ['**/*.{js,jsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + parserOptions: { + ecmaVersion: 'latest', + ecmaFeatures: { jsx: true }, + sourceType: 'module', + }, + }, + settings: { react: { version: '18.3' } }, + plugins: { + react, + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...js.configs.recommended.rules, + ...react.configs.recommended.rules, + ...react.configs['jsx-runtime'].rules, + ...reactHooks.configs.recommended.rules, + 'react/jsx-no-target-blank': 'off', + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + }, +] diff --git a/index.html b/index.html new file mode 100644 index 0000000..f48c6c2 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + Influencer Knowledge Base + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b08cebe --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6396 @@ +{ + "name": "knowledgebase-influencer", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "knowledgebase-influencer", + "version": "0.0.0", + "dependencies": { + "@popperjs/core": "^2.11.8", + "@reduxjs/toolkit": "^2.6.0", + "axios": "^1.8.1", + "bootstrap": "^5.3.3", + "crypto-js": "^4.2.0", + "lodash": "^4.17.21", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.2.0", + "react-syntax-highlighter": "^15.6.1", + "redux-persist": "^6.0.0", + "remark-gfm": "^4.0.1", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "sass": "^1.62.1", + "vite": "^6.2.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.8", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.8.tgz", + "integrity": "sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.9.tgz", + "integrity": "sha512-lWBYIrF7qK5+GjY5Uy+/hEgp8OJWOD/rpy74GplYRhEauvbHDeFB8t5hPOZxCZ0Oxf4Cc36tK51/l3ymJysrKw==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/helper-compilation-targets": "^7.26.5", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/traverse": "^7.26.9", + "@babel/types": "^7.26.9", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", + "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.9.tgz", + "integrity": "sha512-Mz/4+y8udxBKdmzt/UjPACs4G3j5SshJJEFFKxlCGPydG4JAHXxjWjAwjd09tf6oINvl1VfMJo+nB7H2YKQ0dA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", + "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", + "dev": true, + "dependencies": { + "@babel/types": "^7.26.9" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.9.tgz", + "integrity": "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.9.tgz", + "integrity": "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", + "integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", + "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/parser": "^7.26.9", + "@babel/types": "^7.26.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", + "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.9", + "@babel/parser": "^7.26.9", + "@babel/template": "^7.26.9", + "@babel/types": "^7.26.9", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", + "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz", + "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz", + "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz", + "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz", + "integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz", + "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz", + "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz", + "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz", + "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz", + "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz", + "integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz", + "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz", + "integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz", + "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz", + "integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz", + "integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz", + "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz", + "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz", + "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz", + "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz", + "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz", + "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz", + "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz", + "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz", + "integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz", + "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", + "integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", + "integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.0.tgz", + "integrity": "sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.21.0.tgz", + "integrity": "sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", + "integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.12.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.2.tgz", + "integrity": "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.6.0.tgz", + "integrity": "sha512-mWJCYpewLRyTuuzRSEC/IwIBBkYg2dKtQas8mty5MaV2iXzcmicS3gW554FDeOvLnY3x13NIk8MB1e8wHO7rqQ==", + "dependencies": { + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.8.tgz", + "integrity": "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.8.tgz", + "integrity": "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.8.tgz", + "integrity": "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.8.tgz", + "integrity": "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.8.tgz", + "integrity": "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.8.tgz", + "integrity": "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.8.tgz", + "integrity": "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.8.tgz", + "integrity": "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.8.tgz", + "integrity": "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.8.tgz", + "integrity": "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.8.tgz", + "integrity": "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.8.tgz", + "integrity": "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.8.tgz", + "integrity": "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.8.tgz", + "integrity": "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.8.tgz", + "integrity": "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.8.tgz", + "integrity": "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.8.tgz", + "integrity": "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.8.tgz", + "integrity": "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.8.tgz", + "integrity": "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" + }, + "node_modules/@types/react": { + "version": "19.0.10", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.10.tgz", + "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.0.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.0.4.tgz", + "integrity": "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.0.0" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", + "integrity": "sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==", + "dev": true, + "dependencies": { + "@babel/core": "^7.26.0", + "@babel/plugin-transform-react-jsx-self": "^7.25.9", + "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.8.1.tgz", + "integrity": "sha512-NN+fvwH/kV01dYUQ3PTOZns4LWtWhOFCAhQ/pHb88WQ1hNe5V/dvFwc4VJcDL11LT9xSX0QtsR8sWUuyOuOq7g==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bootstrap": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", + "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001701", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001701.tgz", + "integrity": "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.1.0.tgz", + "integrity": "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.105", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.105.tgz", + "integrity": "sha512-ccp7LocdXx3yBhwiG0qTQ7XFrK48Ua2pxIxBdJO8cbddp/MvbBtPFzvnTchtyHQTsgqqczO8cdmAIbpMa0u2+g==", + "dev": true + }, + "node_modules/es-abstract": { + "version": "1.23.9", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", + "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.0", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-regex": "^1.2.1", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.3", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.18" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz", + "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.0", + "@esbuild/android-arm": "0.25.0", + "@esbuild/android-arm64": "0.25.0", + "@esbuild/android-x64": "0.25.0", + "@esbuild/darwin-arm64": "0.25.0", + "@esbuild/darwin-x64": "0.25.0", + "@esbuild/freebsd-arm64": "0.25.0", + "@esbuild/freebsd-x64": "0.25.0", + "@esbuild/linux-arm": "0.25.0", + "@esbuild/linux-arm64": "0.25.0", + "@esbuild/linux-ia32": "0.25.0", + "@esbuild/linux-loong64": "0.25.0", + "@esbuild/linux-mips64el": "0.25.0", + "@esbuild/linux-ppc64": "0.25.0", + "@esbuild/linux-riscv64": "0.25.0", + "@esbuild/linux-s390x": "0.25.0", + "@esbuild/linux-x64": "0.25.0", + "@esbuild/netbsd-arm64": "0.25.0", + "@esbuild/netbsd-x64": "0.25.0", + "@esbuild/openbsd-arm64": "0.25.0", + "@esbuild/openbsd-x64": "0.25.0", + "@esbuild/sunos-x64": "0.25.0", + "@esbuild/win32-arm64": "0.25.0", + "@esbuild/win32-ia32": "0.25.0", + "@esbuild/win32-x64": "0.25.0" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.21.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.21.0.tgz", + "integrity": "sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.2", + "@eslint/core": "^0.12.0", + "@eslint/eslintrc": "^3.3.0", + "@eslint/js": "9.21.0", + "@eslint/plugin-kit": "^0.2.7", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.4.tgz", + "integrity": "sha512-BGP0jRmfYyvOyvMoRX/uoUeW+GqNj9y16bPQzqAHf3AYII/tDs+jMN0dBVkl88/OZwNGwrVFxE7riHsXVfy/LQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.8", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0.tgz", + "integrity": "sha512-mpJRtPgHN2tNAvZ35AMfqeB3Xqeo273QxrHJsbBEPWODRM4r0yB6jfoROqKEYrOn27UtRPpcpHc2UqyBSuUNTw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.19.tgz", + "integrity": "sha512-eyy8pcr/YxSYjBoqIFSrlbn9i/xvxUFa8CjzAYo9cFjgGXqq1hyjihcpZvxRLalpaWmueWR81xn7vuKmAFijDQ==", + "dev": true, + "peerDependencies": { + "eslint": ">=8.40" + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", + "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.8.tgz", + "integrity": "sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dev": true, + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/property-information": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.0.0.tgz", + "integrity": "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz", + "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", + "dependencies": { + "scheduler": "^0.25.0" + }, + "peerDependencies": { + "react": "^19.0.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.2.0.tgz", + "integrity": "sha512-fXyqzPgCPZbqhrk7k3hPcCpYIlQ2ugIXDboHUzhJISFVy2DEPsmHgN588MyGmkIOv3jDgNfUE3kJi83L28s/LQ==", + "dependencies": { + "@types/cookie": "^0.6.0", + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0", + "turbo-stream": "2.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.2.0.tgz", + "integrity": "sha512-cU7lTxETGtQRQbafJubvZKHEn5izNABxZhBY0Jlzdv0gqQhCPQt2J8aN5ZPjS6mQOXn5NnirWNh+FpE8TTYN0Q==", + "dependencies": { + "react-router": "7.2.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-persist": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/redux-persist/-/redux-persist-6.0.0.tgz", + "integrity": "sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ==", + "peerDependencies": { + "redux": ">4.0.0" + } + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, + "node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.34.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", + "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.34.8", + "@rollup/rollup-android-arm64": "4.34.8", + "@rollup/rollup-darwin-arm64": "4.34.8", + "@rollup/rollup-darwin-x64": "4.34.8", + "@rollup/rollup-freebsd-arm64": "4.34.8", + "@rollup/rollup-freebsd-x64": "4.34.8", + "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", + "@rollup/rollup-linux-arm-musleabihf": "4.34.8", + "@rollup/rollup-linux-arm64-gnu": "4.34.8", + "@rollup/rollup-linux-arm64-musl": "4.34.8", + "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", + "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", + "@rollup/rollup-linux-riscv64-gnu": "4.34.8", + "@rollup/rollup-linux-s390x-gnu": "4.34.8", + "@rollup/rollup-linux-x64-gnu": "4.34.8", + "@rollup/rollup-linux-x64-musl": "4.34.8", + "@rollup/rollup-win32-arm64-msvc": "4.34.8", + "@rollup/rollup-win32-ia32-msvc": "4.34.8", + "@rollup/rollup-win32-x64-msvc": "4.34.8", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sass": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.62.1.tgz", + "integrity": "sha512-NHpxIzN29MXvWiuswfc1W3I0N8SXBd8UR26WntmDlRYf0bSADnwnOjsyMZ3lMezSlArD33Vs3YFhp7dWvL770A==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz", + "integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/style-to-js": { + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.16.tgz", + "integrity": "sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==", + "dependencies": { + "style-to-object": "1.0.8" + } + }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/turbo-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", + "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.0.tgz", + "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.25.0", + "postcss": "^8.5.3", + "rollup": "^4.30.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a329be0 --- /dev/null +++ b/package.json @@ -0,0 +1,42 @@ +{ + "name": "knowledgebase-influencer", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@popperjs/core": "^2.11.8", + "@reduxjs/toolkit": "^2.6.0", + "axios": "^1.8.1", + "bootstrap": "^5.3.3", + "crypto-js": "^4.2.0", + "lodash": "^4.17.21", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-markdown": "^10.1.0", + "react-redux": "^9.2.0", + "react-router-dom": "^7.2.0", + "react-syntax-highlighter": "^15.6.1", + "redux-persist": "^6.0.0", + "remark-gfm": "^4.0.1", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.21.0", + "@types/react": "^19.0.10", + "@types/react-dom": "^19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "eslint": "^9.21.0", + "eslint-plugin-react": "^7.37.4", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.19", + "globals": "^15.15.0", + "sass": "^1.62.1", + "vite": "^6.2.0" + } +} diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..5c3f9c3 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,61 @@ +import { useDispatch, useSelector } from 'react-redux'; +import AppRouter from './router/router'; +import { checkAuthThunk } from './store/auth/auth.thunk'; +import { useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { login } from './store/auth/auth.slice'; +import { initWebSocket, closeWebSocket } from './services/websocket'; +import { setWebSocketConnected } from './store/notificationCenter/notificationCenter.slice'; + +function App() { + const navigate = useNavigate(); + const dispatch = useDispatch(); + + const { user } = useSelector((state) => state.auth); + const { isConnected } = useSelector((state) => state.notificationCenter); + + // 检查用户认证状态 + useEffect(() => { + handleCheckAuth(); + }, [dispatch]); + + // 管理WebSocket连接 + useEffect(() => { + console.log(user, isConnected); + + // 如果用户已认证但WebSocket未连接,则初始化连接 + if (user && !isConnected) { + initWebSocket() + .then(() => { + dispatch(setWebSocketConnected(true)); + console.log('WebSocket connection initialized'); + }) + .catch((error) => { + console.error('Failed to initialize WebSocket connection:', error); + }); + } + + // 组件卸载或用户登出时关闭WebSocket连接 + return () => { + if (isConnected) { + closeWebSocket(); + dispatch(setWebSocketConnected(false)); + } + }; + }, [user, isConnected, dispatch]); + + const handleCheckAuth = async () => { + console.log('app handleCheckAuth'); + try { + await dispatch(checkAuthThunk()).unwrap(); + if (!user) navigate('/login'); + } catch (error) { + console.log('error', error); + navigate('/login'); + } + }; + + return ; +} + +export default App; diff --git a/src/assets/react.svg b/src/assets/react.svg new file mode 100644 index 0000000..22c3eba --- /dev/null +++ b/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/AccessRequestModal.jsx b/src/components/AccessRequestModal.jsx new file mode 100644 index 0000000..6a8654c --- /dev/null +++ b/src/components/AccessRequestModal.jsx @@ -0,0 +1,232 @@ +import React, { useState } from 'react'; +import SvgIcon from './SvgIcon'; + +/** + * 申请权限弹窗组件 + * @param {Object} props + * @param {boolean} props.show - 是否显示弹窗 + * @param {string} props.knowledgeBaseId - 知识库ID + * @param {string} props.knowledgeBaseTitle - 知识库标题 + * @param {Function} props.onClose - 关闭弹窗的回调函数 + * @param {Function} props.onSubmit - 提交申请的回调函数,接收 requestData 参数 + * @param {boolean} props.isSubmitting - 是否正在提交 + */ +export default function AccessRequestModal({ + show, + knowledgeBaseId, + knowledgeBaseTitle, + onClose, + onSubmit, + isSubmitting = false, +}) { + const [accessRequestData, setAccessRequestData] = useState({ + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + duration: '30', // 默认30天 + reason: '', + }); + const [accessRequestErrors, setAccessRequestErrors] = useState({}); + + const handleAccessRequestInputChange = (e) => { + const { name, value } = e.target; + if (name === 'duration') { + setAccessRequestData((prev) => ({ + ...prev, + [name]: value, + })); + } else if (name === 'reason') { + setAccessRequestData((prev) => ({ + ...prev, + [name]: value, + })); + } + + // Clear error when user types + if (accessRequestErrors[name]) { + setAccessRequestErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + const handlePermissionChange = (permissionType) => { + setAccessRequestData((prev) => ({ + ...prev, + permissions: { + can_read: true, // 只读权限始终为true + can_edit: permissionType === '编辑权限', + can_delete: false, // 管理权限暂时不开放 + }, + })); + }; + + const validateAccessRequestForm = () => { + const errors = {}; + + if (!accessRequestData.reason.trim()) { + errors.reason = '请输入申请原因'; + } + + setAccessRequestErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleSubmitAccessRequest = () => { + // Validate form + if (!validateAccessRequestForm()) { + return; + } + + // 计算过期日期 + const expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + parseInt(accessRequestData.duration)); + const expiresAt = expirationDate.toISOString(); + + // 调用父组件的提交函数 + onSubmit({ + knowledge_base: knowledgeBaseId, + permissions: accessRequestData.permissions, + reason: accessRequestData.reason, + expires_at: expiresAt, + }); + + // 重置表单 + setAccessRequestData({ + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + }, + duration: '30', + reason: '', + }); + setAccessRequestErrors({}); + }; + + if (!show) return null; + + return ( +
+
+
+
申请访问权限
+ +
+
+
+ + +
+
+ + +
+ +
+ + +
+
+ + + {accessRequestErrors.reason && ( +
{accessRequestErrors.reason}
+ )} +
+
+
+ + +
+
+
+ ); +} diff --git a/src/components/ApiModeSwitch.jsx b/src/components/ApiModeSwitch.jsx new file mode 100644 index 0000000..1dbc4a6 --- /dev/null +++ b/src/components/ApiModeSwitch.jsx @@ -0,0 +1,83 @@ +import React, { useState, useEffect } from 'react'; +import { switchToMockApi, switchToRealApi, checkServerStatus } from '../services/api'; + +export default function ApiModeSwitch() { + const [isMockMode, setIsMockMode] = useState(false); + const [isChecking, setIsChecking] = useState(false); + const [showNotification, setShowNotification] = useState(false); + const [notification, setNotification] = useState({ message: '', type: 'info' }); + + // 组件加载时检查服务器状态 + useEffect(() => { + const checkStatus = async () => { + setIsChecking(true); + const isServerUp = await checkServerStatus(); + setIsMockMode(!isServerUp); + setIsChecking(false); + }; + + checkStatus(); + }, []); + + // 切换API模式 + const handleToggleMode = async () => { + setIsChecking(true); + + if (isMockMode) { + // 尝试切换回真实API + const isServerUp = await switchToRealApi(); + if (isServerUp) { + setIsMockMode(false); + showNotificationMessage('已切换到真实API模式', 'success'); + } else { + showNotificationMessage('服务器连接失败,继续使用模拟数据', 'warning'); + } + } else { + // 切换到模拟API + switchToMockApi(); + setIsMockMode(true); + showNotificationMessage('已切换到模拟API模式', 'info'); + } + + setIsChecking(false); + }; + + // 显示通知消息 + const showNotificationMessage = (message, type) => { + setNotification({ message, type }); + setShowNotification(true); + + // 3秒后自动隐藏通知 + setTimeout(() => { + setShowNotification(false); + }, 3000); + }; + + return ( +
+
+
+ + +
+ {isMockMode && 使用本地模拟数据} + {!isMockMode && 已连接到后端服务器} +
+ + {showNotification && ( +
+ {notification.message} +
+ )} +
+ ); +} diff --git a/src/components/CodeBlock.jsx b/src/components/CodeBlock.jsx new file mode 100644 index 0000000..a33afe3 --- /dev/null +++ b/src/components/CodeBlock.jsx @@ -0,0 +1,41 @@ +import React, { useState } from 'react'; +import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'; +import { atomDark } from 'react-syntax-highlighter/dist/esm/styles/prism'; +import SvgIcon from './SvgIcon'; + +/** + * CodeBlock component renders a syntax highlighted code block with a copy button + */ +const CodeBlock = ({ language, value }) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + navigator.clipboard.writeText(value).then(() => { + setCopied(true); + // Reset the copied state after 2 seconds + setTimeout(() => setCopied(false), 2000); + }); + }; + + return ( +
+
+ {language && {language}} + +
+ + {value} + +
+ ); +}; + +export default CodeBlock; diff --git a/src/components/CreateKnowledgeBaseModal.jsx b/src/components/CreateKnowledgeBaseModal.jsx new file mode 100644 index 0000000..73893b0 --- /dev/null +++ b/src/components/CreateKnowledgeBaseModal.jsx @@ -0,0 +1,287 @@ +import React, { useState, useEffect } from 'react'; +import SvgIcon from './SvgIcon'; + +// 部门和组别的映射关系 +const departmentGroups = { + 达人部门: ['达人'], + 商务部门: ['商务'], + 样本中心: ['样本'], + 产品部门: ['产品'], + AI自媒体: ['AI自媒体'], + HR: ['HR'], + 技术部门: ['技术'], +}; + +// 部门列表 +const departments = Object.keys(departmentGroups); + +/** + * 创建知识库模态框组件 + * @param {Object} props + * @param {boolean} props.show - 是否显示弹窗 + * @param {Object} props.formData - 表单数据 + * @param {Object} props.formErrors - 表单错误信息 + * @param {boolean} props.isSubmitting - 是否正在提交 + * @param {Function} props.onClose - 关闭弹窗的回调函数 + * @param {Function} props.onChange - 表单输入变化的回调函数 + * @param {Function} props.onSubmit - 提交表单的回调函数 + * @param {Object} props.currentUser - 当前用户信息 + */ +const CreateKnowledgeBaseModal = ({ + show, + formData, + formErrors, + isSubmitting, + onClose, + onChange, + onSubmit, + currentUser, +}) => { + // 根据用户角色确定可以创建的知识库类型 + const isAdmin = currentUser?.role === 'admin'; + const isLeader = currentUser?.role === 'leader'; + + // 获取当前用户的部门和组别 + const userDepartment = currentUser?.department || ''; + + // 可选的组别列表 + const [availableGroups, setAvailableGroups] = useState([]); + + // 当部门变化时更新可用的组别 + useEffect(() => { + if (formData.department && departmentGroups[formData.department]) { + setAvailableGroups(departmentGroups[formData.department]); + } else { + setAvailableGroups([]); + } + }, [formData.department]); + + // 提前返回需要放在所有 Hooks 之后 + if (!show) return null; + + // 获取当前用户可以创建的知识库类型 + const getAvailableTypes = () => { + if (isAdmin) { + return [ + { value: 'admin', label: '公共知识库' }, + { value: 'leader', label: '组长级知识库' }, + { value: 'member', label: '组内知识库' }, + { value: 'private', label: '私有知识库' }, + { value: 'secret', label: '私密知识库' }, + ]; + } else if (isLeader) { + return [ + { value: 'admin', label: '公共知识库' }, + { value: 'member', label: '组内知识库' }, + { value: 'private', label: '私有知识库' }, + ]; + } else { + return [ + { value: 'admin', label: '公共知识库' }, + { value: 'private', label: '私有知识库' }, + ]; + } + }; + + const availableTypes = getAvailableTypes(); + + // 判断是否需要选择组别 + const needDepartmentAndGroup = formData.type === 'member' || formData.type === 'leader'; + const needSelectGroup = needDepartmentAndGroup; + + return ( +
+
+
+
新建知识库
+ +
+
+
+ + + {formErrors.name &&
{formErrors.name}
} +
+
+ + + {formErrors.desc &&
{formErrors.desc}
} +
+
+ +
+ {availableTypes.map((type, index) => ( +
+ + +
+ ))} +
+ {!isAdmin && !isLeader && ( + + 您可以创建公共知识库(所有人可访问)或私有知识库(仅自己可访问)。 + + )} + {formErrors.type &&
{formErrors.type}
} +
+ + {/* 仅当不是私有知识库且需要部门和组别时才显示部门选项 */} + {needDepartmentAndGroup && ( +
+ + {isAdmin ? ( + // 管理员可以选择任意部门 + + ) : ( + // 非管理员显示只读字段 + + )} + {formErrors.department && ( +
{formErrors.department}
+ )} +
+ )} + + {/* 仅当不是私有知识库且需要部门和组别时才显示组别选项 */} + {needDepartmentAndGroup && ( +
+ + {isAdmin || (isLeader && needSelectGroup) ? ( + // 管理员可以选择任意组别,组长只能选择自己部门下的组别 + + ) : ( + // 普通用户显示只读字段 + + )} + {formErrors.group &&
{formErrors.group}
} +
+ )} +
+
+ + +
+
+
+ ); +}; + +export default CreateKnowledgeBaseModal; diff --git a/src/components/ErrorBoundary.jsx b/src/components/ErrorBoundary.jsx new file mode 100644 index 0000000..cdacf09 --- /dev/null +++ b/src/components/ErrorBoundary.jsx @@ -0,0 +1,47 @@ +import React, { Component } from 'react'; + +/** + * Error Boundary component to catch errors in child components + * and display a fallback UI instead of crashing the whole app + */ +class ErrorBoundary extends Component { + constructor(props) { + super(props); + this.state = { hasError: false, error: null }; + } + + static getDerivedStateFromError(error) { + // Update state so the next render will show the fallback UI + return { hasError: true, error }; + } + + componentDidCatch(error, errorInfo) { + // You can log the error to an error reporting service + console.error('Error caught by ErrorBoundary:', error); + console.error('Component stack:', errorInfo.componentStack); + } + + render() { + const { children, fallback } = this.props; + + if (this.state.hasError) { + // You can render any custom fallback UI + if (typeof fallback === 'function') { + return fallback(this.state.error); + } + + return ( + fallback || ( +
+

Error rendering content

+ The content couldn't be displayed properly. +
+ ) + ); + } + + return children; + } +} + +export default ErrorBoundary; diff --git a/src/components/Loading.jsx b/src/components/Loading.jsx new file mode 100644 index 0000000..edf13e6 --- /dev/null +++ b/src/components/Loading.jsx @@ -0,0 +1,11 @@ +import React from 'react'; + +const Loading = () => { + return ( +
+ Loading... +
+ ); +}; + +export default Loading; diff --git a/src/components/NotificationCenter.jsx b/src/components/NotificationCenter.jsx new file mode 100644 index 0000000..dbf5372 --- /dev/null +++ b/src/components/NotificationCenter.jsx @@ -0,0 +1,310 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + clearNotifications, + markAllNotificationsAsRead, + markNotificationAsRead, + setWebSocketConnected, +} from '../store/notificationCenter/notificationCenter.slice'; +import RequestDetailSlideOver from '../pages/Permissions/components/RequestDetailSlideOver'; +import { approvePermissionThunk, rejectPermissionThunk } from '../store/permissions/permissions.thunks'; +import { showNotification } from '../store/notification.slice'; +import { initWebSocket, acknowledgeNotification, closeWebSocket } from '../services/websocket'; + +export default function NotificationCenter({ show, onClose }) { + const [showAll, setShowAll] = useState(false); + const dispatch = useDispatch(); + const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter); + const [selectedRequest, setSelectedRequest] = useState(null); + const [showSlideOver, setShowSlideOver] = useState(false); + const [showResponseInput, setShowResponseInput] = useState(false); + const [currentRequestId, setCurrentRequestId] = useState(null); + const [isApproving, setIsApproving] = useState(false); + const [responseMessage, setResponseMessage] = useState(''); + const { isAuthenticated } = useSelector((state) => state.auth); + + const displayedNotifications = showAll ? notifications : notifications.slice(0, 5); + + // 初始化WebSocket连接 + useEffect(() => { + // 只有在用户已登录的情况下才连接WebSocket + if (isAuthenticated && !isConnected) { + initWebSocket() + .then(() => { + dispatch(setWebSocketConnected(true)); + console.log('Successfully connected to notification WebSocket'); + }) + .catch((error) => { + console.error('Failed to connect to notification WebSocket:', error); + dispatch(setWebSocketConnected(false)); + // 可以在这里显示连接失败的通知 + dispatch( + showNotification({ + message: '通知服务连接失败,部分功能可能不可用', + type: 'warning', + }) + ); + }); + } + + // 组件卸载时关闭WebSocket连接 + return () => { + if (isConnected) { + closeWebSocket(); + dispatch(setWebSocketConnected(false)); + } + }; + }, [isAuthenticated, isConnected, dispatch]); + + const handleClearAll = () => { + dispatch(clearNotifications()); + }; + + const handleMarkAllAsRead = () => { + dispatch(markAllNotificationsAsRead()); + }; + + const handleMarkAsRead = (notificationId) => { + dispatch(markNotificationAsRead(notificationId)); + // 同时发送确认消息到服务器 + acknowledgeNotification(notificationId); + }; + + const handleViewDetail = (notification) => { + // 标记为已读 + if (!notification.isRead) { + handleMarkAsRead(notification.id); + } + + if (notification.type === 'permission') { + setSelectedRequest(notification); + setShowSlideOver(true); + } + }; + + const handleCloseSlideOver = () => { + setShowSlideOver(false); + setTimeout(() => { + setSelectedRequest(null); + }, 300); + }; + + const handleOpenResponseInput = (requestId, approving) => { + setCurrentRequestId(requestId); + setIsApproving(approving); + setShowResponseInput(true); + }; + + const handleCloseResponseInput = () => { + setShowResponseInput(false); + setCurrentRequestId(null); + setResponseMessage(''); + }; + + const handleProcessRequest = () => { + if (!currentRequestId) return; + + const params = { + id: currentRequestId, + responseMessage, + }; + + if (isApproving) { + dispatch(approvePermissionThunk(params)); + } else { + dispatch(rejectPermissionThunk(params)); + } + }; + + if (!show) return null; + + return ( + <> +
+
+
+
通知中心
+ {unreadCount > 0 && {unreadCount}} + {isConnected ? ( + 已连接 + ) : ( + 未连接 + )} +
+
+ + + +
+
+
+ {displayedNotifications.length === 0 ? ( +
+ +

暂无通知

+
+ ) : ( + displayedNotifications.map((notification) => ( +
+
+
+ +
+
+
+
+ {notification.title} +
+ {notification.time} +
+

{notification.content}

+
+ {notification.hasDetail && ( + + )} + {!notification.isRead && ( + + )} +
+
+
+
+ )) + )} +
+ {notifications.length > 5 && ( +
+ +
+ )} +
+ + {/* 使用滑动面板组件 */} + handleOpenResponseInput(id, true)} + onReject={(id) => handleOpenResponseInput(id, false)} + processingId={currentRequestId} + approveRejectStatus={showResponseInput ? 'loading' : 'idle'} + isApproving={isApproving} + /> + + {/* 回复输入弹窗 */} + {showResponseInput && ( +
+
+
+
+
{isApproving ? '批准' : '拒绝'}申请
+ +
+
+
+ + +
+
+
+ + +
+
+
+
+
+ )} + + ); +} diff --git a/src/components/NotificationSnackbar.jsx b/src/components/NotificationSnackbar.jsx new file mode 100644 index 0000000..61d29c9 --- /dev/null +++ b/src/components/NotificationSnackbar.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import { useSelector, useDispatch } from 'react-redux'; +import { hideNotification } from '../store/notification.slice.js'; +import Snackbar from './Snackbar.jsx'; + +const NotificationSnackbar = () => { + const notification = useSelector((state) => state.notification); + const dispatch = useDispatch(); + + if (!notification) return null; // 没有通知时不渲染 + + return ( + dispatch(hideNotification())} + /> + ); +}; + +export default NotificationSnackbar; diff --git a/src/components/Pagination.jsx b/src/components/Pagination.jsx new file mode 100644 index 0000000..e16ab79 --- /dev/null +++ b/src/components/Pagination.jsx @@ -0,0 +1,59 @@ +import React from 'react'; + +/** + * 分页组件 + * @param {Object} props + * @param {number} props.currentPage - 当前页码 + * @param {number} props.totalPages - 总页数 + * @param {number} props.pageSize - 每页显示的条目数 + * @param {Function} props.onPageChange - 页码变化的回调函数 + * @param {Function} props.onPageSizeChange - 每页条目数变化的回调函数 + */ +const Pagination = ({ currentPage, totalPages, pageSize, onPageChange, onPageSizeChange }) => { + return ( +
+
+ +
+ +
+ ); +}; + +export default Pagination; diff --git a/src/components/SafeMarkdown.jsx b/src/components/SafeMarkdown.jsx new file mode 100644 index 0000000..7eedb1b --- /dev/null +++ b/src/components/SafeMarkdown.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; +import ErrorBoundary from './ErrorBoundary'; +import CodeBlock from './CodeBlock'; + +/** + * SafeMarkdown component that wraps ReactMarkdown with error handling + * Displays raw content as fallback if markdown parsing fails + */ +const SafeMarkdown = ({ content, className = 'markdown-content' }) => { + // Fallback UI that shows raw content when ReactMarkdown fails + const renderFallback = (error) => { + console.error('Markdown rendering error:', error); + return ( +
+

+ Error rendering markdown. Showing raw content: +

+
{content}
+
+ ); + }; + + return ( + +
, + code({ node, inline, className: codeClassName, children, ...props }) { + const match = /language-(\w+)/.exec(codeClassName || ''); + return !inline && match ? ( + + ) : ( + + {children} + + ); + }, + }} + > + {content} + + + ); +}; + +export default SafeMarkdown; diff --git a/src/components/SearchBar.jsx b/src/components/SearchBar.jsx new file mode 100644 index 0000000..cefac8c --- /dev/null +++ b/src/components/SearchBar.jsx @@ -0,0 +1,215 @@ +import React, { useRef, useState, useEffect } from 'react'; +import SvgIcon from './SvgIcon'; + +/** + * 搜索栏组件 + * @param {Object} props + * @param {string} props.searchKeyword - 搜索关键词 + * @param {boolean} props.isSearching - 是否正在搜索 + * @param {Function} props.onSearchChange - 搜索关键词变化的回调函数 + * @param {Function} props.onSearch - 提交搜索的回调函数 + * @param {Function} props.onClearSearch - 清除搜索的回调函数 + * @param {string} props.placeholder - 搜索框占位文本 + * @param {string} props.className - 额外的 CSS 类名 + * @param {Array} props.searchResults - 搜索结果 + * @param {boolean} props.isSearchLoading - 搜索是否正在加载 + * @param {Function} props.onResultClick - 点击搜索结果的回调 + * @param {Function} props.onRequestAccess - 申请权限的回调 + * @param {string} props.cornerStyle - 设置圆角风格,可选值: 'rounded'(圆角) 或 'square'(方角) + */ +const SearchBar = ({ + searchKeyword, + isSearching, + onSearchChange, + onSearch, + onClearSearch, + placeholder = '搜索知识库...', + className = 'w-50', + searchResults = [], + isSearchLoading = false, + onResultClick, + onRequestAccess, + cornerStyle = 'rounded', // 默认为圆角 +}) => { + const [showDropdown, setShowDropdown] = useState(false); + const searchRef = useRef(null); + const inputRef = useRef(null); + + // 计算边框圆角样式类 + const getBorderRadiusClass = () => { + return cornerStyle === 'rounded' ? 'rounded-pill' : 'rounded-0'; + }; + + // 处理点击外部关闭下拉框 + useEffect(() => { + const handleClickOutside = (event) => { + if (searchRef.current && !searchRef.current.contains(event.target)) { + setShowDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + // 只有在用户执行搜索后才显示下拉框,无论是否有结果 + useEffect(() => { + if (isSearching) { + setShowDropdown(true); + } + }, [isSearching]); + + // 处理输入变化 + const handleInputChange = (e) => { + onSearchChange(e); + }; + + // 处理搜索提交 + const handleSubmit = (e) => { + e.preventDefault(); + onSearch(e); + // 搜索提交后,如果有关键词,将显示下拉框(由searchResults决定) + }; + + return ( +
+
+
+ + {searchKeyword.trim() && ( + + )} + +
+
+ + {/* 搜索结果下拉框 - 在用户搜索后显示,无论是否有结果 */} + {showDropdown && ( +
+
+ {isSearchLoading ? ( +
+
+ 加载中... +
+ 搜索中... +
+ ) : searchResults?.length > 0 ? ( + <> +
+ 搜索结果 ({searchResults.length}) +
+ {searchResults.map((item) => ( +
+
+
{ + if (item.permissions?.can_read) { + onResultClick(item.id, item.permissions); + setShowDropdown(false); + } + }} + > +
+ + + {item.highlighted_name ? ( + + ) : ( + item.name + )} + +
+
+ + {item.type === 'private' ? '私有' : item.type} + + {item.department && {item.department}} + {!item.permissions?.can_read && ( + + + 无权限 + + )} +
+
+ {!item.permissions?.can_read && ( + + )} +
+
+ ))} + + ) : ( +
未找到匹配的知识库
+ )} +
+
+ )} +
+ ); +}; + +export default SearchBar; diff --git a/src/components/Snackbar.jsx b/src/components/Snackbar.jsx new file mode 100644 index 0000000..eae061b --- /dev/null +++ b/src/components/Snackbar.jsx @@ -0,0 +1,41 @@ +import React, { useEffect } from 'react'; +import SvgIcon from './SvgIcon'; + +const Snackbar = ({ type = 'primary', message, duration = 3000, onClose }) => { + if (!message) return null; + + useEffect(() => { + if (message) { + const timer = setTimeout(() => { + if (onClose) onClose(); + }, duration); + return () => clearTimeout(timer); + } + }, [message, duration, onClose]); + + const icons = { + success: 'check-circle-fill', + primary: 'info-fill', + warning: 'exclamation-triangle-fill', + danger: 'exclamation-triangle-fill', + }; + + // 处理关闭按钮点击 + const handleClose = (e) => { + e.preventDefault(); + if (onClose) onClose(); + }; + + return ( +
+ +
{message}
+ +
+ ); +}; + +export default Snackbar; diff --git a/src/components/SvgIcon.jsx b/src/components/SvgIcon.jsx new file mode 100644 index 0000000..61d2b0c --- /dev/null +++ b/src/components/SvgIcon.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { icons } from '../icons/icons'; + +export default function SvgIcon({ className, width, height, color, style }) { + // Create a new SVG string with custom attributes if provided + const customizeSvg = (svgString) => { + if (!svgString) return ''; + + // If no customization needed, return the original SVG + if (!width && !height && !color) return svgString; + + // Parse the SVG to modify attributes + let modifiedSvg = svgString; + + // Replace width if provided + if (width) { + modifiedSvg = modifiedSvg.replace(/width=['"]([^'"]*)['"]/g, `width="${width}"`); + } + + // Replace height if provided + if (height) { + modifiedSvg = modifiedSvg.replace(/height=['"]([^'"]*)['"]/g, `height="${height}"`); + } + + // Replace fill color if provided + if (color) { + modifiedSvg = modifiedSvg.replace(/fill=['"]currentColor['"]/g, `fill="${color}"`); + } + + return modifiedSvg; + }; + + return ( + + ); +} diff --git a/src/components/UserSettingsModal.jsx b/src/components/UserSettingsModal.jsx new file mode 100644 index 0000000..a2059e3 --- /dev/null +++ b/src/components/UserSettingsModal.jsx @@ -0,0 +1,268 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import '../styles/style.scss'; +import { updateProfileThunk } from '../store/auth/auth.thunk'; + +// 部门和组别的映射关系 +const departmentGroups = { + 达人部门: ['达人'], + 商务部门: ['商务'], + 样本中心: ['样本'], + 产品部门: ['产品'], + AI自媒体: ['AI自媒体'], + HR: ['HR'], + 技术部门: ['技术'], +}; + +function UserSettingsModal({ show, onClose }) { + const { user, loading } = useSelector((state) => state.auth); + const [lastPasswordChange] = useState('30天前'); // This would come from backend in real app + const [formData, setFormData] = useState({}); + // 可选的组别列表 + const [availableGroups, setAvailableGroups] = useState([]); + + const [submitted, setSubmitted] = useState(false); + const [errors, setErrors] = useState({}); + + const dispatch = useDispatch(); + + useEffect(() => { + if (user) { + setFormData({ + name: user.name, + email: user.email, + department: user.department, + group: user.group, + }); + } + }, [user]); + + // 当部门变化时更新可用的组别 + useEffect(() => { + if (formData.department && departmentGroups[formData.department]) { + setAvailableGroups(departmentGroups[formData.department]); + } else { + setAvailableGroups([]); + } + }, [formData.department]); + + if (!show) return null; + + const handleInputChange = (e) => { + const { name, value } = e.target; + + if (name === 'department') { + setFormData({ + ...formData, + [name]: value, + ['group']: '', + }); + } else { + setFormData({ + ...formData, + [name]: value, + }); + } + + // 清除对应的错误信息 + if (errors[name]) { + setErrors({ + ...errors, + [name]: '', + }); + } + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setSubmitted(true); + + if (validateForm()) { + console.log('Form submitted successfully!'); + console.log('Update data:', formData); + try { + await dispatch(updateProfileThunk(formData)).unwrap(); + } catch (error) { + console.error('Signup failed:', error); + } + } + }; + + const validateForm = () => { + const newErrors = {}; + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formData.email)) { + newErrors.email = 'Invalid email address'; + } + + if (!formData.name) { + newErrors.name = 'Name is required'; + } + + if (!formData.department) { + newErrors.department = '请选择部门'; + } + + if (!formData.group) { + newErrors.group = '请选择组别'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + return ( +
+
+
+
+
个人设置
+ +
+
+
+
个人信息
+
+ + +
+
+ + + {submitted && errors.name &&
{errors.name}
} +
+
+ + + {submitted && errors.email &&
{errors.email}
} +
+
+ + {submitted && errors.department && ( +
{errors.department}
+ )} +
+
+ + {submitted && errors.group &&
{errors.group}
} +
+
+ +
+
安全设置
+
+
+
+ + 修改密码 +
+ 上次修改:{lastPasswordChange} +
+ +
+
+
+
+ + 双重认证 +
+ 增强账户安全性 +
+ +
+
+ +
+
通知设置
+
+ + +
新的数据集访问申请通知
+
+
+ + +
异常登录和权限变更提醒
+
+
+
+
+ + +
+
+
+
+ ); +} + +export default UserSettingsModal; diff --git a/src/icons/icons.js b/src/icons/icons.js new file mode 100644 index 0000000..fa296b6 --- /dev/null +++ b/src/icons/icons.js @@ -0,0 +1,125 @@ +export const icons = { + plus: ` + + `, + 'more-dot': ` + + `, + trash: ` + + + `, + 'check-circle-fill': ` + + `, + 'info-fill': ` + + `, + 'exclamation-triangle-fill': ` + + `, + file: ` + + `, + clock: ` + + `, + 'circle-yes': ` + + `, + eye: ` + + `, + 'chat-dot': ` + + `, + key: ` + + `, + lock: ` + + `, + 'stack-fill': ` + + `, + edit: ``, + list: ` + + `, + 'setting-fill': ``, + dataset: ``, + calendar: ``, + clipboard: ``, + chat: ``, + 'arrowup-upload': ``, + send: ``, + search: ``, + bell: ``, + 'magnifying-glass': ``, + close: ` + + `, + 'knowledge-base': ` + + + + `, + 'knowledge-base-large': ` + + + + `, + plus: ` + + `, + building: ``, + group:`` +}; diff --git a/src/layouts/HeaderWithNav.jsx b/src/layouts/HeaderWithNav.jsx new file mode 100644 index 0000000..b945efe --- /dev/null +++ b/src/layouts/HeaderWithNav.jsx @@ -0,0 +1,168 @@ +import React, { useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; +import { logoutThunk } from '../store/auth/auth.thunk'; +import UserSettingsModal from '../components/UserSettingsModal'; +import NotificationCenter from '../components/NotificationCenter'; +import SvgIcon from '../components/SvgIcon'; + +export default function HeaderWithNav() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const location = useLocation(); + const { user } = useSelector((state) => state.auth); + const [showSettings, setShowSettings] = useState(false); + const [showNotifications, setShowNotifications] = useState(false); + const { notifications, unreadCount, isConnected } = useSelector((state) => state.notificationCenter); + + const handleLogout = async () => { + try { + await dispatch(logoutThunk()).unwrap(); + sessionStorage.removeItem('token'); + navigate('/login'); + } catch (error) {} + }; + + // Check if the current path starts with the given path + const isActive = (path) => { + return location.pathname.startsWith(path); + }; + console.log('user', user); + + // 检查用户是否有管理权限(leader 或 admin) + const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin'); + + return ( +
+ + setShowSettings(false)} /> + setShowNotifications(false)} /> +
+ ); +} diff --git a/src/layouts/Mainlayout.jsx b/src/layouts/Mainlayout.jsx new file mode 100644 index 0000000..76ef448 --- /dev/null +++ b/src/layouts/Mainlayout.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import HeaderWithNav from './HeaderWithNav'; +import '../styles/style.scss'; +import NotificationSnackbar from '../components/NotificationSnackbar'; + +export default function Mainlayout({ children }) { + return ( + <> + + {children} + + ); +} diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..8203d0a --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,22 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import './styles/base.scss'; +import App from './App.jsx'; +import { BrowserRouter } from 'react-router-dom'; +import 'bootstrap'; +import { Provider } from 'react-redux'; +import store, { persistor } from './store/store.js'; +import { PersistGate } from 'redux-persist/integration/react'; +import Loading from './components/Loading.jsx'; + +createRoot(document.getElementById('root')).render( + // + } persistor={persistor}> + + + + + + + // +); diff --git a/src/pages/Chat/Chat.jsx b/src/pages/Chat/Chat.jsx new file mode 100644 index 0000000..ae16ed3 --- /dev/null +++ b/src/pages/Chat/Chat.jsx @@ -0,0 +1,179 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchChats, deleteChat, createChatRecord, createConversation } from '../../store/chat/chat.thunks'; +import { showNotification } from '../../store/notification.slice'; +import ChatSidebar from './ChatSidebar'; +import NewChat from './NewChat'; +import ChatWindow from './ChatWindow'; + +export default function Chat() { + const { knowledgeBaseId, chatId } = useParams(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + // 从 Redux store 获取聊天记录列表 + const { + items: chatHistory, + status, + error, + } = useSelector((state) => state.chat.history || { items: [], status: 'idle', error: null }); + const operationStatus = useSelector((state) => state.chat.createSession?.status); + const operationError = useSelector((state) => state.chat.createSession?.error); + + // 获取聊天记录列表 + useEffect(() => { + dispatch(fetchChats({ page: 1, page_size: 20 })); + }, [dispatch]); + + // 监听操作状态,显示通知 + useEffect(() => { + if (operationStatus === 'succeeded') { + dispatch( + showNotification({ + message: '操作成功', + type: 'success', + }) + ); + } else if (operationStatus === 'failed' && operationError) { + dispatch( + showNotification({ + message: `操作失败: ${operationError}`, + type: 'danger', + }) + ); + } + }, [operationStatus, operationError, dispatch]); + + // If we have a knowledgeBaseId but no chatId, check if we have an existing chat or create a new one + useEffect(() => { + // 只有当 knowledgeBaseId 存在但 chatId 不存在,且聊天历史已加载完成时才执行 + if (knowledgeBaseId && !chatId && status === 'succeeded' && !status.includes('loading')) { + console.log('Chat.jsx: 检查是否需要创建聊天...'); + + // 处理可能的多个知识库ID (以逗号分隔) + const knowledgeBaseIds = knowledgeBaseId.split(',').map((id) => id.trim()); + console.log('Chat.jsx: 处理知识库ID列表:', knowledgeBaseIds); + + // 检查是否存在包含所有选中知识库的聊天记录 + const existingChat = chatHistory.find((chat) => { + // 没有datasets属性或不是数组,跳过 + if (!chat.datasets || !Array.isArray(chat.datasets)) { + return false; + } + + // 获取当前聊天记录中的知识库ID列表 + const chatDatasetIds = chat.datasets.map((ds) => ds.id); + + // 检查所有选中的知识库是否都包含在这个聊天中 + // 并且聊天中的知识库数量要和选中的相同(完全匹配) + return ( + knowledgeBaseIds.length === chatDatasetIds.length && + knowledgeBaseIds.every((id) => chatDatasetIds.includes(id)) + ); + }); + + console.log('Chat.jsx: existingChat', existingChat); + + if (existingChat) { + console.log( + `Chat.jsx: 找到现有聊天记录,导航到 /chat/${knowledgeBaseId}/${existingChat.conversation_id}` + ); + // 找到现有聊天记录,导航到该聊天页面 + navigate(`/chat/${knowledgeBaseId}/${existingChat.conversation_id}`); + } else { + console.log('Chat.jsx: 创建新聊天...'); + // 创建新聊天 - 使用新的API创建会话 + dispatch( + createConversation({ + dataset_id_list: knowledgeBaseIds, + }) + ) + .unwrap() + .then((response) => { + // 创建成功,使用返回的conversation_id导航 + if (response && response.conversation_id) { + console.log( + `Chat.jsx: 创建成功,导航到 /chat/${knowledgeBaseId}/${response.conversation_id}` + ); + navigate(`/chat/${knowledgeBaseId}/${response.conversation_id}`); + } else { + // 错误处理 + console.error('Chat.jsx: 创建失败,未能获取会话ID'); + dispatch( + showNotification({ + message: '创建聊天失败:未能获取会话ID', + type: 'danger', + }) + ); + } + }) + .catch((error) => { + console.error('Chat.jsx: 创建失败', error); + dispatch( + showNotification({ + message: `创建聊天失败: ${error}`, + type: 'danger', + }) + ); + }); + } + } + }, [knowledgeBaseId, chatId, chatHistory, status, navigate, dispatch]); + + const handleDeleteChat = (id) => { + // 调用 Redux action 删除聊天 + dispatch(deleteChat(id)) + .unwrap() + .then(() => { + // 删除成功后显示通知 + dispatch( + showNotification({ + message: '聊天记录已删除', + type: 'success', + }) + ); + + // If the deleted chat is the current one, navigate to the chat list + if (chatId === id) { + navigate('/chat'); + } + }) + .catch((error) => { + // 删除失败显示错误通知 + dispatch( + showNotification({ + message: `删除失败: ${error}`, + type: 'danger', + }) + ); + }); + }; + + return ( +
+
+ {/* Sidebar */} +
+ +
+ + {/* Main Content */} +
+ {!chatId ? : } +
+
+
+ ); +} diff --git a/src/pages/Chat/ChatSidebar.jsx b/src/pages/Chat/ChatSidebar.jsx new file mode 100644 index 0000000..766482d --- /dev/null +++ b/src/pages/Chat/ChatSidebar.jsx @@ -0,0 +1,134 @@ +import React, { useState } from 'react'; +import { Link, useNavigate, useParams } from 'react-router-dom'; +import SvgIcon from '../../components/SvgIcon'; + +export default function ChatSidebar({ chatHistory = [], onDeleteChat, isLoading = false, hasError = false }) { + const navigate = useNavigate(); + const { chatId, knowledgeBaseId } = useParams(); + const [activeDropdown, setActiveDropdown] = useState(null); + + const handleNewChat = () => { + navigate('/chat'); + }; + + const handleToggleDropdown = (e, id) => { + e.preventDefault(); + e.stopPropagation(); + setActiveDropdown(activeDropdown === id ? null : id); + }; + + const handleDeleteChat = (e, id) => { + e.preventDefault(); + e.stopPropagation(); + if (onDeleteChat) { + onDeleteChat(id); + } + setActiveDropdown(null); + }; + + // 渲染加载状态 + const renderLoading = () => ( +
+
+ 加载中... +
+
加载聊天记录...
+
+ ); + + // 渲染错误状态 + const renderError = () => ( +
+
+ +
+
加载聊天记录失败,请重试
+
+ ); + + // 渲染空状态 + const renderEmpty = () => ( +
+
暂无聊天记录
+
+ ); + + return ( +
+
+
聊天记录
+
+ +
+ +
+ +
+ {isLoading ? ( + renderLoading() + ) : hasError ? ( + renderError() + ) : chatHistory.length === 0 ? ( + renderEmpty() + ) : ( +
    + {chatHistory.map((chat) => ( +
  • + ds.id).join(',') || knowledgeBaseId}/${ + chat.conversation_id + }`} + className={`text-decoration-none d-flex align-items-center gap-2 py-2 text-dark ${ + chatId === chat.conversation_id ? 'fw-bold' : '' + }`} + > +
    +
    + {chat.datasets?.map((ds) => ds.name).join(', ') || '未命名知识库'} +
    +
    + +
    + + {activeDropdown === chat.conversation_id && ( +
    + +
    + )} +
    +
  • + ))} +
+ )} +
+
+ ); +} diff --git a/src/pages/Chat/ChatWindow.jsx b/src/pages/Chat/ChatWindow.jsx new file mode 100644 index 0000000..82ecf2a --- /dev/null +++ b/src/pages/Chat/ChatWindow.jsx @@ -0,0 +1,403 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchMessages } from '../../store/chat/chat.messages.thunks'; +import { resetMessages, resetSendMessageStatus, addMessage } from '../../store/chat/chat.slice'; +import { showNotification } from '../../store/notification.slice'; +import { createChatRecord, fetchAvailableDatasets, fetchConversationDetail } from '../../store/chat/chat.thunks'; +import { fetchKnowledgeBases } from '../../store/knowledgeBase/knowledgeBase.thunks'; +import SvgIcon from '../../components/SvgIcon'; +import SafeMarkdown from '../../components/SafeMarkdown'; +import { get } from '../../services/api'; + +export default function ChatWindow({ chatId, knowledgeBaseId }) { + const dispatch = useDispatch(); + const [inputMessage, setInputMessage] = useState(''); + const [loading, setLoading] = useState(false); + const messagesEndRef = useRef(null); + const hasLoadedDetailRef = useRef({}); // 添加ref来跟踪已加载的会话 + + // 从 Redux store 获取消息 + const messages = useSelector((state) => state.chat.messages.items); + const messageStatus = useSelector((state) => state.chat.messages.status); + const messageError = useSelector((state) => state.chat.messages.error); + const { status: sendStatus, error: sendError } = useSelector((state) => state.chat.sendMessage); + + // 使用新的Redux状态结构 + const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases || []); + const knowledgeBase = knowledgeBases.find((kb) => kb.id === knowledgeBaseId); + const isLoadingKnowledgeBases = useSelector((state) => state.knowledgeBase.loading); + + // 获取可用数据集列表 + const availableDatasets = useSelector((state) => state.chat.availableDatasets.items || []); + const availableDatasetsLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading'); + + // 获取会话详情 + const conversation = useSelector((state) => state.chat.currentChat.data); + const conversationStatus = useSelector((state) => state.chat.currentChat.status); + const conversationError = useSelector((state) => state.chat.currentChat.error); + + // 获取会话创建状态 + const createSessionStatus = useSelector((state) => state.chat.createSession?.status); + const createSessionId = useSelector((state) => state.chat.createSession?.sessionId); + + // 监听知识库ID变更,确保保存在组件状态中 + const [selectedKnowledgeBaseIds, setSelectedKnowledgeBaseIds] = useState([]); + + // 当conversation或knowledgeBaseId更新时,更新selectedKnowledgeBaseIds + useEffect(() => { + // 优先使用conversation中的知识库列表 + if (conversation && conversation.datasets && conversation.datasets.length > 0) { + const datasetIds = conversation.datasets.map((ds) => ds.id); + console.log('从会话中获取知识库列表:', datasetIds); + setSelectedKnowledgeBaseIds(datasetIds); + } + // 其次使用URL中传入的知识库ID + else if (knowledgeBaseId) { + // 可能是单个ID或以逗号分隔的多个ID + const ids = knowledgeBaseId.split(',').map((id) => id.trim()); + console.log('从URL参数中获取知识库列表:', ids); + setSelectedKnowledgeBaseIds(ids); + } + }, [conversation, knowledgeBaseId]); + + // 获取聊天详情 + useEffect(() => { + if (!chatId) return; + + // 如果已经加载过这个chatId的详情,不再重复加载 + if (hasLoadedDetailRef.current[chatId]) { + console.log('跳过已加载过的会话详情:', chatId); + return; + } + + // 检查是否是新创建的会话 + const isNewlyCreatedChat = createSessionStatus === 'succeeded' && createSessionId === chatId; + + // 如果是新创建的会话且已经有会话数据,则跳过详情获取 + if (isNewlyCreatedChat && conversation && conversation.conversation_id === chatId) { + console.log('跳过新创建会话的详情获取:', chatId); + hasLoadedDetailRef.current[chatId] = true; + return; + } + + console.log('获取会话详情:', chatId); + setLoading(true); + + dispatch(fetchConversationDetail(chatId)) + .unwrap() + .then((response) => { + console.log('获取会话详情成功:', response); + // 标记为已加载 + hasLoadedDetailRef.current[chatId] = true; + }) + .catch((error) => { + console.error('获取会话详情失败:', error); + dispatch( + showNotification({ + message: `获取聊天详情失败: ${error || '未知错误'}`, + type: 'danger', + }) + ); + }) + .finally(() => { + setLoading(false); + }); + + // 组件卸载时清空消息 + return () => { + dispatch(resetMessages()); + // 不要清空hasLoadedDetailRef,否则会导致重复加载 + // hasLoadedDetailRef.current = {}; // 清理ref缓存 + }; + }, [chatId, dispatch, createSessionStatus, createSessionId]); + + // 组件销毁时完全清空ref缓存 + useEffect(() => { + return () => { + hasLoadedDetailRef.current = {}; + }; + }, []); + + // 新会话自动添加欢迎消息 + useEffect(() => { + // 如果是新聊天且没有任何消息,添加一条系统欢迎消息 + if (chatId && messages.length === 0 && !loading && messageStatus !== 'loading') { + const selectedKb = knowledgeBase || + availableDatasets.find((ds) => ds.id === knowledgeBaseId) || { name: '知识库' }; + + dispatch( + addMessage({ + id: 'welcome-' + Date.now(), + role: 'assistant', + content: `欢迎使用${selectedKb.name}。您可以向我提问任何相关问题。`, + created_at: new Date().toISOString(), + }) + ); + } + }, [chatId, messages.length, loading, messageStatus, knowledgeBase, knowledgeBaseId, availableDatasets, dispatch]); + + // 监听发送消息状态 + useEffect(() => { + if (sendStatus === 'failed' && sendError) { + dispatch( + showNotification({ + message: `发送失败: ${sendError}`, + type: 'danger', + }) + ); + dispatch(resetSendMessageStatus()); + } + }, [sendStatus, sendError, dispatch]); + + // 滚动到底部 + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + // 获取当前会话的知识库信息 + useEffect(() => { + // 如果conversation有数据集信息,优先使用它 + if (conversation && conversation.datasets && conversation.datasets.length > 0) { + return; + } + + // 如果没有会话数据集信息,但有knowledgeBaseId,尝试从知识库列表中查找 + if (knowledgeBaseId && knowledgeBases.length === 0 && !availableDatasets.length) { + dispatch(fetchAvailableDatasets()); + } + }, [dispatch, knowledgeBaseId, knowledgeBases, conversation, availableDatasets]); + + const handleSendMessage = (e) => { + e.preventDefault(); + + if (!inputMessage.trim() || sendStatus === 'loading') return; + + console.log('准备发送消息:', inputMessage); + console.log('当前会话ID:', chatId); + + // 获取知识库ID列表 + let dataset_id_list = []; + + // 优先使用组件状态中保存的知识库列表 + if (selectedKnowledgeBaseIds.length > 0) { + // 使用已保存的知识库列表 + dataset_id_list = selectedKnowledgeBaseIds.map((id) => id.replace(/-/g, '')); + console.log('使用组件状态中的知识库列表:', dataset_id_list); + } else if (conversation && conversation.datasets && conversation.datasets.length > 0) { + // 如果已有会话,使用会话中的知识库 + dataset_id_list = conversation.datasets.map((ds) => ds.id.replace(/-/g, '')); + console.log('使用会话中的知识库列表:', dataset_id_list); + } else if (knowledgeBaseId) { + // 如果是新会话,使用当前选择的知识库 + // 可能是单个ID或以逗号分隔的多个ID + const ids = knowledgeBaseId.split(',').map((id) => id.trim().replace(/-/g, '')); + dataset_id_list = ids; + console.log('使用URL参数中的知识库:', dataset_id_list); + } else if (availableDatasets.length > 0) { + // 如果都没有,尝试使用可用知识库列表中的第一个 + dataset_id_list = [availableDatasets[0].id.replace(/-/g, '')]; + console.log('使用可用知识库列表中的第一个:', dataset_id_list); + } + + if (dataset_id_list.length === 0) { + dispatch( + showNotification({ + message: '发送失败:未选择知识库', + type: 'danger', + }) + ); + return; + } + + console.log('发送消息参数:', { + dataset_id_list, + question: inputMessage, + conversation_id: chatId, + }); + + // 发送消息到服务器 + dispatch( + createChatRecord({ + dataset_id_list: dataset_id_list, + question: inputMessage, + conversation_id: chatId, + }) + ) + .unwrap() + .then((response) => { + // 成功发送后,可以执行任何需要的操作 + console.log('消息发送成功:', response); + }) + .catch((error) => { + // 发送失败,显示错误信息 + console.error('消息发送失败:', error); + dispatch( + showNotification({ + message: `发送失败: ${error}`, + type: 'danger', + }) + ); + }); + + // 清空输入框 + setInputMessage(''); + }; + + // 渲染加载状态 + const renderLoading = () => ( +
+
+ 加载中... +
+
加载聊天记录...
+
+ ); + + // 渲染错误状态 + const renderError = () => ( +
+

+ 加载消息失败 +

+

{messageError}

+ +
+ ); + + // 渲染空消息状态 + const renderEmpty = () => { + if (loading) return null; + + return ( +
+

暂无消息,开始发送第一条消息吧

+
+ ); + }; + + return ( +
+ {/* Chat header */} +
+ {conversation && conversation.datasets ? ( + <> +
{conversation.datasets.map((dataset) => dataset.name).join(', ')}
+ {conversation.datasets.length > 0 && conversation.datasets[0].type && ( + 类型: {conversation.datasets[0].type} + )} + + ) : knowledgeBase ? ( + <> +
{knowledgeBase.name}
+ {knowledgeBase.description} + + ) : ( +
{loading || availableDatasetsLoading ? '加载中...' : '聊天'}
+ )} +
+ + {/* Chat messages */} +
+
+ {messageStatus === 'loading' + ? renderLoading() + : messageStatus === 'failed' + ? renderError() + : messages.length === 0 + ? renderEmpty() + : messages.map((message) => ( +
+
+
+ {message.role === 'user' ? ( + message.content + ) : ( + + )} + {message.is_streaming && ( + + + + + + )} +
+
+
+ {message.created_at && + (() => { + const messageDate = new Date(message.created_at); + const today = new Date(); + + // 检查是否是今天 + const isToday = + messageDate.getDate() === today.getDate() && + messageDate.getMonth() === today.getMonth() && + messageDate.getFullYear() === today.getFullYear(); + + // 如果是今天,只显示时间;否则显示年月日和时间 + if (isToday) { + return messageDate.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit', + }); + } else { + return messageDate.toLocaleString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + } + })()} + {message.is_streaming && ' · 正在生成...'} +
+
+ ))} + +
+
+
+ + {/* Chat input */} +
+
+ setInputMessage(e.target.value)} + disabled={sendStatus === 'loading'} + /> + +
+
+
+ ); +} diff --git a/src/pages/Chat/NewChat.jsx b/src/pages/Chat/NewChat.jsx new file mode 100644 index 0000000..809d1de --- /dev/null +++ b/src/pages/Chat/NewChat.jsx @@ -0,0 +1,256 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { showNotification } from '../../store/notification.slice'; +import { fetchAvailableDatasets, fetchChats, createConversation } from '../../store/chat/chat.thunks'; +import SvgIcon from '../../components/SvgIcon'; + +export default function NewChat() { + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [selectedDatasetIds, setSelectedDatasetIds] = useState([]); + const [isNavigating, setIsNavigating] = useState(false); + + // 从 Redux store 获取可用知识库数据 + const datasets = useSelector((state) => state.chat.availableDatasets.items || []); + const isLoading = useSelector((state) => state.chat.availableDatasets.status === 'loading'); + const error = useSelector((state) => state.chat.availableDatasets.error); + + // 获取聊天历史记录 + const chatHistory = useSelector((state) => state.chat.history.items || []); + const chatHistoryLoading = useSelector((state) => state.chat.history.status === 'loading'); + const chatCreationStatus = useSelector((state) => state.chat.sendMessage?.status); + + // 获取可用知识库列表和聊天历史 + useEffect(() => { + dispatch(fetchAvailableDatasets()); + dispatch(fetchChats({ page: 1, page_size: 50 })); + }, [dispatch]); + + // 监听错误状态 + useEffect(() => { + if (error) { + dispatch( + showNotification({ + message: `获取可用知识库列表失败: ${error}`, + type: 'danger', + }) + ); + } + }, [error, dispatch]); + + // 处理知识库选择(切换选择状态) + const handleToggleKnowledgeBase = (dataset) => { + if (isNavigating) return; // 如果正在导航中,阻止操作 + + setSelectedDatasetIds((prev) => { + // 检查是否已经选中 + const isSelected = prev.includes(dataset.id); + + if (isSelected) { + // 如果已选中,则从数组中移除 + return prev.filter((id) => id !== dataset.id); + } else { + // 如果未选中,则添加到数组中 + return [...prev, dataset.id]; + } + }); + }; + + // 开始聊天 + const handleStartChat = async () => { + if (selectedDatasetIds.length === 0) { + dispatch( + showNotification({ + message: '请至少选择一个知识库', + type: 'warning', + }) + ); + return; + } + + if (isNavigating) return; // 防止重复点击 + + try { + setIsNavigating(true); + + // 打印调试信息 + console.log('选中的知识库ID:', selectedDatasetIds); + + // 检查是否已存在包含所有选中知识库的聊天记录 + // 注意:这里的逻辑简化了,实际可能需要更复杂的匹配算法 + const existingChat = chatHistory.find((chat) => { + // 检查聊天记录中的知识库是否完全匹配当前选择 + if (chat.datasets && Array.isArray(chat.datasets)) { + const chatDatasetIds = chat.datasets.map((ds) => ds.id); + return ( + chatDatasetIds.length === selectedDatasetIds.length && + selectedDatasetIds.every((id) => chatDatasetIds.includes(id)) + ); + } + + // 兼容旧格式 + if (chat.dataset_id_list && Array.isArray(chat.dataset_id_list)) { + const formattedSelectedIds = selectedDatasetIds.map((id) => id.replace(/-/g, '')); + return ( + chat.dataset_id_list.length === formattedSelectedIds.length && + formattedSelectedIds.every((id) => chat.dataset_id_list.includes(id)) + ); + } + + return false; + }); + + if (existingChat) { + // 找到现有聊天记录,导航到该聊天页面 + // 使用所有知识库ID作为URL参数,以逗号分隔 + const knowledgeBaseIdsParam = selectedDatasetIds.join(','); + console.log( + `找到现有聊天记录,直接导航到 /chat/${knowledgeBaseIdsParam}/${existingChat.conversation_id}` + ); + navigate(`/chat/${knowledgeBaseIdsParam}/${existingChat.conversation_id}`); + } else { + // 没有找到现有聊天记录,创建新的聊天 + console.log(`未找到现有聊天记录,创建新会话,选中的知识库ID: ${selectedDatasetIds.join(', ')}`); + + try { + // 调用createConversation创建新会话(不发送消息) + const response = await dispatch( + createConversation({ + dataset_id_list: selectedDatasetIds, + }) + ).unwrap(); + + console.log('创建会话响应:', response); + + if (response && response.conversation_id) { + // 使用所有知识库ID作为URL参数,以逗号分隔 + const knowledgeBaseIdsParam = selectedDatasetIds.join(','); + console.log(`创建会话成功,导航到 /chat/${knowledgeBaseIdsParam}/${response.conversation_id}`); + navigate(`/chat/${knowledgeBaseIdsParam}/${response.conversation_id}`); + } else { + throw new Error('未能获取会话ID:' + JSON.stringify(response)); + } + } catch (apiError) { + // 专门处理API调用错误 + console.error('API调用失败:', apiError); + throw new Error(`API调用失败: ${apiError.message || '未知错误'}`); + } + } + } catch (error) { + console.error('导航或创建聊天失败:', error); + + // 添加更详细的错误日志 + if (error.stack) { + console.error('错误堆栈:', error.stack); + } + + // 显示更友好的错误消息 + dispatch( + showNotification({ + message: `创建聊天失败: ${error.message || '请重试'}`, + type: 'danger', + }) + ); + } finally { + setIsNavigating(false); + } + }; + + // 渲染加载状态 + if (isLoading || chatHistoryLoading) { + return ( +
+
+ 加载中... +
+
+ ); + } + + return ( +
+ {/* 导航中的遮罩层 */} + {isNavigating && ( +
+
+
+ 加载中... +
+
正在加载聊天界面...
+
+
+ )} + +
+

选择知识库开始聊天

+ +
+ + {selectedDatasetIds.length > 0 && ( +
已选择 {selectedDatasetIds.length} 个知识库
+ )} + +
+ {datasets.length > 0 ? ( + datasets.map((dataset) => { + const isSelected = selectedDatasetIds.includes(dataset.id); + return ( +
+
handleToggleKnowledgeBase(dataset)} + style={{ opacity: isNavigating && !isSelected ? 0.6 : 1 }} + > +
+
+ {dataset.name} + {isSelected && } +
+

+ {dataset.desc || dataset.description || ''} +

+
+ + {dataset.department || ''} + +
+
+
+
+ ); + }) + ) : ( +
+
暂无可访问的知识库,请先申请知识库访问权限
+
+ )} +
+
+ ); +} diff --git a/src/pages/KnowledgeBase/Detail/DatasetTab.jsx b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx new file mode 100644 index 0000000..4dc4fab --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/DatasetTab.jsx @@ -0,0 +1,386 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useDispatch } from 'react-redux'; +import { showNotification } from '../../../store/notification.slice'; +import SvgIcon from '../../../components/SvgIcon'; + +// 导入拆分的组件 +import Breadcrumb from './components/Breadcrumb'; +import DocumentList from './components/DocumentList'; +import FileUploadModal from './components/FileUploadModal'; +import { getKnowledgeBaseDocuments } from '../../../store/knowledgeBase/knowledgeBase.thunks'; + +export default function DatasetTab({ knowledgeBase }) { + const dispatch = useDispatch(); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedDocuments, setSelectedDocuments] = useState([]); + const [selectAll, setSelectAll] = useState(false); + const [showBatchDropdown, setShowBatchDropdown] = useState(false); + const [showAddFileModal, setShowAddFileModal] = useState(false); + const [isSubmitting, setIsSubmitting] = useState(false); + const [newFile, setNewFile] = useState({ + name: '', + description: '', + file: null, + }); + const [fileErrors, setFileErrors] = useState({}); + const dropdownRef = useRef(null); + const fileInputRef = useRef(null); + + // Use documents from knowledge base or empty array if not available + const [documents, setDocuments] = useState(knowledgeBase.documents || []); + + // Update documents when knowledgeBase changes + useEffect(() => { + setDocuments(knowledgeBase.documents || []); + }, [knowledgeBase]); + + // 获取文档列表 + useEffect(() => { + if (knowledgeBase?.id) { + dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBase.id })); + } + }, [dispatch, knowledgeBase?.id]); + + + // Handle click outside dropdown + useEffect(() => { + function handleClickOutside(event) { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setShowBatchDropdown(false); + } + } + + // Add event listener + document.addEventListener('mousedown', handleClickOutside); + return () => { + // Remove event listener on cleanup + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [dropdownRef]); + + // Handle search input change + const handleSearchChange = (e) => { + setSearchQuery(e.target.value); + }; + + // Handle select all checkbox + const handleSelectAll = () => { + if (selectAll) { + setSelectedDocuments([]); + } else { + setSelectedDocuments(documents.map((doc) => doc.id)); + } + setSelectAll(!selectAll); + }; + + // Handle individual document selection + const handleSelectDocument = (docId) => { + if (selectedDocuments.includes(docId)) { + setSelectedDocuments(selectedDocuments.filter((id) => id !== docId)); + setSelectAll(false); + } else { + setSelectedDocuments([...selectedDocuments, docId]); + if (selectedDocuments.length + 1 === documents.length) { + setSelectAll(true); + } + } + }; + + // Handle batch delete + const handleBatchDelete = () => { + if (selectedDocuments.length === 0) return; + + // Here you would typically call an API to delete the selected documents + console.log('Deleting documents:', selectedDocuments); + + // Update documents state by removing selected documents + setDocuments((prevDocuments) => prevDocuments.filter((doc) => !selectedDocuments.includes(doc.id))); + + // Show notification + dispatch( + showNotification({ + message: '已删除选中的数据集', + type: 'success', + }) + ); + + // Reset selection + setSelectedDocuments([]); + setSelectAll(false); + setShowBatchDropdown(false); + }; + + // Handle file input change + const handleFileChange = (e) => { + const selectedFile = e.target.files[0]; + if (selectedFile) { + setNewFile({ + ...newFile, + name: selectedFile.name, + file: selectedFile, + }); + + // Clear file error if exists + if (fileErrors.file) { + setFileErrors((prev) => ({ + ...prev, + file: '', + })); + } + } + }; + + // Handle description input change + const handleDescriptionChange = (e) => { + setNewFile({ + ...newFile, + description: e.target.value, + }); + }; + + // Handle file drop + const handleFileDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + + const droppedFile = e.dataTransfer.files[0]; + if (droppedFile) { + setNewFile({ + ...newFile, + name: droppedFile.name, + file: droppedFile, + }); + + // Clear file error if exists + if (fileErrors.file) { + setFileErrors((prev) => ({ + ...prev, + file: '', + })); + } + } + }; + + // Prevent default behavior for drag events + const handleDragOver = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + // Validate file form + const validateFileForm = () => { + const errors = {}; + + if (!newFile.file) { + errors.file = '请上传文件'; + } + + setFileErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Handle file upload + const handleFileUpload = () => { + // Validate form + if (!validateFileForm()) { + return; + } + + setIsSubmitting(true); + + // Here you would typically call an API to upload the file + console.log('Uploading file:', newFile); + + // Simulate API call + setTimeout(() => { + // Generate a new ID for the document + const newId = Date.now().toString(); + + // Format file size + const fileSizeKB = newFile.file ? (newFile.file.size / 1024).toFixed(0) + 'kb' : '0kb'; + + // Get current date + const today = new Date(); + const formattedDate = today.toISOString(); + + // Create new document object + const newDocument = { + id: newId, + name: newFile.name, + description: newFile.description || '无描述', + size: fileSizeKB, + create_time: formattedDate, + update_time: formattedDate, + }; + + // Add new document to the documents array + setDocuments((prevDocuments) => [...prevDocuments, newDocument]); + + // Show notification + dispatch( + showNotification({ + message: '数据集上传成功', + type: 'success', + }) + ); + + setIsSubmitting(false); + // Reset form and close modal + handleCloseAddFileModal(); + }, 1000); + }; + + // Open file selector when clicking on the upload area + const handleUploadAreaClick = () => { + fileInputRef.current.click(); + }; + + // Handle close modal + const handleCloseAddFileModal = () => { + setNewFile({ + name: '', + description: '', + file: null, + }); + setFileErrors({}); + setShowAddFileModal(false); + }; + + // Handle delete document + const handleDeleteDocument = (docId) => { + // Here you would typically call an API to delete the document + console.log('Deleting document:', docId); + + // Update documents state by removing the deleted document + setDocuments((prevDocuments) => prevDocuments.filter((doc) => doc.id !== docId)); + + // Show notification + dispatch( + showNotification({ + message: '数据集已删除', + type: 'success', + }) + ); + }; + + // Filter documents based on search query + const filteredDocuments = documents.filter( + (doc) => + doc.name.toLowerCase().includes(searchQuery.toLowerCase()) || + (doc.description && doc.description.toLowerCase().includes(searchQuery.toLowerCase())) + ); + + return ( + <> + {/* Breadcrumb navigation */} + + + {/* Toolbar */} +
+
+ + {selectedDocuments.length > 0 && ( +
+ + {showBatchDropdown && ( +
    +
  • + +
  • +
+ )} +
+ )} +
+
+ +
+
+ + {/* Document list */} + + {/* Pagination */} + {/*
+
+ 每页行数: + +
+
+ 1-5 of 10 + +
+
*/} + + {/* File upload modal */} + + + ); +} diff --git a/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx b/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx new file mode 100644 index 0000000..a986f25 --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/KnowledgeBaseDetail.jsx @@ -0,0 +1,119 @@ +import React, { useState, useEffect } from 'react'; +import { useParams, useNavigate } from 'react-router-dom'; +import { useSelector, useDispatch } from 'react-redux'; +import { showNotification } from '../../../store/notification.slice'; +import { getKnowledgeBaseById } from '../../../store/knowledgeBase/knowledgeBase.thunks'; +import SvgIcon from '../../../components/SvgIcon'; +import DatasetTab from './DatasetTab'; +import SettingsTab from './SettingsTab'; + +export default function KnowledgeBaseDetail() { + const { id, tab } = useParams(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [activeTab, setActiveTab] = useState(tab === 'settings' ? 'settings' : 'datasets'); + + // Get knowledge base details from Redux store - 使用新的状态结构 + const knowledgeBase = useSelector((state) => state.knowledgeBase.currentKnowledgeBase); + const loading = useSelector((state) => state.knowledgeBase.loading); + const error = useSelector((state) => state.knowledgeBase.error); + + // Fetch knowledge base details when component mounts or ID changes + useEffect(() => { + if (id) { + dispatch(getKnowledgeBaseById(id)); + } + }, [dispatch, id]); + + // Update active tab when URL changes + useEffect(() => { + if (tab) { + setActiveTab(tab === 'settings' ? 'settings' : 'datasets'); + } + }, [tab]); + + // If knowledge base not found, show notification and redirect + useEffect(() => { + if (!loading && error) { + dispatch( + showNotification({ + message: `获取知识库失败: ${error.message || '未找到知识库'}`, + type: 'warning', + }) + ); + navigate('/knowledge-base'); + } + }, [loading, error, dispatch, navigate]); + + // Handle tab change + const handleTabChange = (tab) => { + setActiveTab(tab); + navigate(`/knowledge-base/${id}/${tab}`); + }; + + // Show loading state if knowledge base not loaded yet + if (loading || !knowledgeBase) { + return ( +
+
+ 加载中... +
+
+ ); + } + + return ( +
+
+ {/* Sidebar */} + + + {/* Main content */} +
+ {/* Render the appropriate tab component */} + {activeTab === 'datasets' ? ( + + ) : ( + + )} +
+
+
+ ); +} diff --git a/src/pages/KnowledgeBase/Detail/SettingsTab.jsx b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx new file mode 100644 index 0000000..d493cc3 --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/SettingsTab.jsx @@ -0,0 +1,351 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { showNotification } from '../../../store/notification.slice'; +import { + updateKnowledgeBase, + deleteKnowledgeBase, + changeKnowledgeBaseType, +} from '../../../store/knowledgeBase/knowledgeBase.thunks'; + +// 导入拆分的组件 +import Breadcrumb from './components/Breadcrumb'; +import KnowledgeBaseForm from './components/KnowledgeBaseForm'; +import DeleteConfirmModal from './components/DeleteConfirmModal'; + +// 部门和组别的映射关系 +const departmentGroups = { + 达人部门: ['达人'], + 商务部门: ['商务'], + 样本中心: ['样本'], + 产品部门: ['产品'], + AI自媒体: ['AI自媒体'], + HR: ['HR'], + 技术部门: ['技术'], +}; + +// 部门列表 +const departments = Object.keys(departmentGroups); + +export default function SettingsTab({ knowledgeBase }) { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const currentUser = useSelector((state) => state.auth.user); + const isAdmin = currentUser?.role === 'admin'; + + // State for knowledge base form + const [knowledgeBaseForm, setKnowledgeBaseForm] = useState({ + permissions: knowledgeBase.permissions, + name: knowledgeBase.name, + desc: knowledgeBase.desc || knowledgeBase.description || '', + type: knowledgeBase.type || 'private', // 默认为私有知识库 + original_type: knowledgeBase.type || 'private', // 存储原始类型用于比较 + department: knowledgeBase.department || '', + group: knowledgeBase.group || '', + original_department: knowledgeBase.department || '', + original_group: knowledgeBase.group || '', + }); + const [formErrors, setFormErrors] = useState({}); + const [isSubmitting, setIsSubmitting] = useState(false); + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); + const [availableGroups, setAvailableGroups] = useState([]); + + // 当部门变化时,更新可选的组别 + useEffect(() => { + if (knowledgeBaseForm.department && departmentGroups[knowledgeBaseForm.department]) { + setAvailableGroups(departmentGroups[knowledgeBaseForm.department]); + // 如果已选择的组别不在新部门的选项中,则重置组别 + if (!departmentGroups[knowledgeBaseForm.department].includes(knowledgeBaseForm.group)) { + setKnowledgeBaseForm((prev) => ({ + ...prev, + group: '', + })); + } + } else { + setAvailableGroups([]); + setKnowledgeBaseForm((prev) => ({ + ...prev, + group: '', + })); + } + }, [knowledgeBaseForm.department]); + + // 初始化可用组别 + useEffect(() => { + if (knowledgeBase.department && departmentGroups[knowledgeBase.department]) { + setAvailableGroups(departmentGroups[knowledgeBase.department]); + } + }, [knowledgeBase]); + + // Handle knowledge base form input change + const handleInputChange = (e) => { + const { name, value } = e.target; + + // 检查如果是类型更改,确保用户有权限 + if (name === 'type') { + const role = currentUser?.role; + let allowed = false; + + // 根据角色判断可以选择的知识库类型 + if (role === 'admin') { + // 管理员可以选择任何类型 + allowed = ['admin', 'leader', 'member', 'private', 'secret'].includes(value); + } else if (role === 'leader') { + // 组长只能选择 member 和 private + allowed = ['admin', 'member', 'private'].includes(value); + } else { + // 普通成员只能选择公共和private + allowed = ['admin', 'private'].includes(value); + } + + if (!allowed) { + dispatch( + showNotification({ + message: '您没有权限设置此类型的知识库', + type: 'warning', + }) + ); + return; + } + } + + setKnowledgeBaseForm((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error if exists + if (formErrors[name]) { + setFormErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + // Validate knowledge base form + const validateForm = () => { + const errors = {}; + + if (!knowledgeBaseForm.name.trim()) { + errors.name = '请输入知识库名称'; + } + + if (!knowledgeBaseForm.desc.trim()) { + errors.desc = '请输入知识库描述'; + } + + if (!knowledgeBaseForm.type) { + errors.type = '请选择知识库类型'; + } + + // 只有非私有知识库需要验证部门和组别 + if ( + (knowledgeBaseForm.type === 'leader' || knowledgeBaseForm.type === 'member') && + !knowledgeBaseForm.department + ) { + errors.department = '请选择部门'; + } + if (knowledgeBaseForm.type === 'member' && !knowledgeBaseForm.group) { + errors.group = '请选择组别'; + } + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + // 检查是否有部门或组别变更 + const hasDepartmentOrGroupChanged = () => { + return ( + knowledgeBaseForm.department !== knowledgeBaseForm.original_department || + knowledgeBaseForm.group !== knowledgeBaseForm.original_group + ); + }; + + // 单独处理类型更改 + const handleTypeChange = (newType) => { + if (!currentUser) { + dispatch( + showNotification({ + message: '用户信息不完整,无法更改类型', + type: 'warning', + }) + ); + return; + } + + // 所有用户都可以修改为admin或private类型,无需额外检查 + if (newType !== 'admin' && newType !== 'private' && currentUser.role === 'member') { + dispatch( + showNotification({ + message: '您只能将知识库修改为公共(admin)或私有(private)类型', + type: 'warning', + }) + ); + return; + } + + if (isAdmin && !validateForm()) { + return; + } + console.log(newType, currentUser.role); + + setIsSubmitting(true); + + // 私有知识库不需要部门和组别 + const isPrivate = newType === 'private'; + const department = isPrivate ? '' : isAdmin ? knowledgeBaseForm.department : currentUser.department || ''; + const group = isPrivate ? '' : isAdmin ? knowledgeBaseForm.group : currentUser.group || ''; + console.log(newType, currentUser.role); + + dispatch( + changeKnowledgeBaseType({ + id: knowledgeBase.id, + type: newType, + department, + group, + }) + ) + .unwrap() + .then((updatedKB) => { + // 更新表单显示 + setKnowledgeBaseForm((prev) => ({ + ...prev, + type: updatedKB.type, + original_type: updatedKB.type, + department: updatedKB.department, + original_department: updatedKB.department, + group: updatedKB.group, + original_group: updatedKB.group, + })); + + dispatch( + showNotification({ + message: `知识库类型已更新为 ${updatedKB.type}`, + type: 'success', + }) + ); + setIsSubmitting(false); + }) + .catch((error) => { + dispatch( + showNotification({ + message: `类型更新失败: ${error || '未知错误'}`, + type: 'danger', + }) + ); + setIsSubmitting(false); + }); + }; + + // Handle form submission + const handleSubmit = (e) => { + e.preventDefault(); + + // Validate form + if (!validateForm()) { + return; + } + + setIsSubmitting(true); + + // 检查类型是否有更改,如果有,则使用单独的API更新类型 + if (knowledgeBaseForm.type !== knowledgeBaseForm.original_type || (isAdmin && hasDepartmentOrGroupChanged())) { + handleTypeChange(knowledgeBaseForm.type); + return; + } + + // Dispatch update knowledge base action (不包含类型更改) + dispatch( + updateKnowledgeBase({ + id: knowledgeBase.id, + data: { + name: knowledgeBaseForm.name, + desc: knowledgeBaseForm.desc, + description: knowledgeBaseForm.desc, // Add description field for compatibility + }, + }) + ) + .unwrap() + .then(() => { + dispatch( + showNotification({ + message: '知识库更新成功', + type: 'success', + }) + ); + setIsSubmitting(false); + }) + .catch((error) => { + dispatch( + showNotification({ + message: `更新失败: ${error || '未知错误'}`, + type: 'danger', + }) + ); + setIsSubmitting(false); + }); + }; + + // Handle knowledge base deletion + const handleDelete = () => { + setIsSubmitting(true); + + // Dispatch delete knowledge base action + dispatch(deleteKnowledgeBase(knowledgeBase.id)) + .unwrap() + .then(() => { + dispatch( + showNotification({ + message: '知识库已删除', + type: 'success', + }) + ); + // Navigate back to knowledge base list + // Redux store 已在 reducer 中更新,不需要重新调用接口获取知识库列表 + navigate('/knowledge-base'); + }) + .catch((error) => { + dispatch( + showNotification({ + message: `删除失败: ${error || '未知错误'}`, + type: 'danger', + }) + ); + setIsSubmitting(false); + setShowDeleteConfirm(false); + }); + }; + + return ( + <> + {/* Breadcrumb navigation */} + + + {/* Knowledge Base Form */} + setShowDeleteConfirm(true)} + onTypeChange={handleTypeChange} + isAdmin={isAdmin} + departments={departments} + availableGroups={availableGroups} + /> + + {/* User Permissions Manager */} + {/* */} + + {/* Delete confirmation modal */} + setShowDeleteConfirm(false)} + onConfirm={handleDelete} + /> + + ); +} diff --git a/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx new file mode 100644 index 0000000..b3bc408 --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/Breadcrumb.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +/** + * 面包屑导航组件 + */ +const Breadcrumb = ({ knowledgeBase, activeTab }) => { + return ( +
+ +
+ ); +}; + +export default Breadcrumb; diff --git a/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx new file mode 100644 index 0000000..0d3c1fa --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/DeleteConfirmModal.jsx @@ -0,0 +1,70 @@ +import React from 'react'; + +/** + * 删除确认模态框组件 + */ +const DeleteConfirmModal = ({ show, title, isSubmitting, onCancel, onConfirm }) => { + if (!show) return null; + + return ( +
+
+
+
确认删除
+ +
+
+

您确定要删除知识库 "{title}" 吗?此操作不可撤销。

+
+
+ + +
+
+
+ ); +}; + +export default DeleteConfirmModal; diff --git a/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx new file mode 100644 index 0000000..690ce55 --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/DocumentList.jsx @@ -0,0 +1,222 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { formatDate } from '../../../../utils/dateUtils'; +import { deleteKnowledgeBaseDocument } from '../../../../store/knowledgeBase/knowledgeBase.thunks'; +import DocumentPreviewModal from './DocumentPreviewModal'; + +/** + * 知识库文档列表组件 + */ +const DocumentList = ({ knowledgeBaseId }) => { + const dispatch = useDispatch(); + const { items, loading, error } = useSelector((state) => state.knowledgeBase.documents); + const currentKnowledgeBase = useSelector((state) => state.knowledgeBase.currentKnowledgeBase); + const [previewModalVisible, setPreviewModalVisible] = useState(false); + const [selectedDocumentId, setSelectedDocumentId] = useState(null); + + // 前端分页状态 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + const [displayedItems, setDisplayedItems] = useState([]); + const [totalPages, setTotalPages] = useState(1); + + // 检查当前用户是否有编辑权限 + const canEdit = currentKnowledgeBase?.permissions?.can_edit || false; + + // 处理分页 + useEffect(() => { + if (!items || items.length === 0) { + setDisplayedItems([]); + setTotalPages(1); + return; + } + + // 计算总页数 + const total = Math.ceil(items.length / pageSize); + setTotalPages(total); + + // 确保当前页有效 + let page = currentPage; + if (page > total) { + page = total; + setCurrentPage(page); + } + + // 计算当前页显示的项目 + const startIndex = (page - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, items.length); + setDisplayedItems(items.slice(startIndex, endIndex)); + + console.log(`前端分页: 总项目 ${items.length}, 当前页 ${page}/${total}, 显示 ${startIndex + 1}-${endIndex}`); + }, [items, currentPage, pageSize]); + + // 分页控制 + const handlePageChange = (page) => { + setCurrentPage(page); + }; + + const handlePrevPage = () => { + if (currentPage > 1) { + setCurrentPage(currentPage - 1); + } + }; + + const handleNextPage = () => { + if (currentPage < totalPages) { + setCurrentPage(currentPage + 1); + } + }; + + // 调试输出 + useEffect(() => { + console.log('DocumentList 渲染 - 知识库ID:', knowledgeBaseId); + console.log('文档列表状态:', { items, loading, error }); + console.log('文档列表项数:', items ? items.length : 0); + }, [knowledgeBaseId, items, loading, error]); + + const handleDeleteDocument = (documentId) => { + if (window.confirm('确定要删除此文档吗?')) { + dispatch( + deleteKnowledgeBaseDocument({ + knowledge_base_id: knowledgeBaseId, + document_id: documentId, + }) + ); + } + }; + + const handlePreviewDocument = (documentId) => { + setSelectedDocumentId(documentId); + setPreviewModalVisible(true); + }; + + const handleClosePreviewModal = () => { + setPreviewModalVisible(false); + setSelectedDocumentId(null); + }; + + if (loading) { + console.log('DocumentList - 加载中...'); + return ( +
+
+ 加载中... +
+
+ ); + } + + if (!items || items.length === 0) { + console.log('DocumentList - 暂无文档'); + return ( +
+

暂无文档,请上传文档

+
+ ); + } + + console.log('DocumentList - 渲染文档列表', displayedItems); + return ( + <> +
+ + + + + + + + + + + {displayedItems.map((doc) => ( + + + + + + + ))} + +
文档名称创建时间更新时间操作
+
+ {doc.document_name || doc.name} +
+
{formatDateTime(doc.create_time || doc.created_at)}{formatDateTime(doc.update_time || doc.updated_at)} +
+ + {canEdit && ( + + )} +
+
+ + {/* 分页控件 */} + {items.length > 0 && ( +
+
+ 共 {items.length} 条记录,第 {currentPage}/{totalPages} 页 +
+ +
+ )} +
+ + + + ); +}; + +// Helper function to format date string +const formatDateTime = (dateString) => { + if (!dateString) return '-'; + + // If the utility function exists, use it, otherwise format manually + try { + return formatDate(dateString); + } catch (error) { + const date = new Date(dateString); + return date.toLocaleString(); + } +}; + +export default DocumentList; diff --git a/src/pages/KnowledgeBase/Detail/components/DocumentPreviewModal.jsx b/src/pages/KnowledgeBase/Detail/components/DocumentPreviewModal.jsx new file mode 100644 index 0000000..8b8598b --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/DocumentPreviewModal.jsx @@ -0,0 +1,132 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { get } from '../../../../services/api'; +import { showNotification } from '../../../../store/notification.slice'; + +/** + * 文档预览模态框组件 + */ +const DocumentPreviewModal = ({ show, documentId, knowledgeBaseId, onClose }) => { + const dispatch = useDispatch(); + const [documentContent, setDocumentContent] = useState(null); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (show && documentId && knowledgeBaseId) { + fetchDocumentContent(); + } + }, [show, documentId, knowledgeBaseId]); + + const fetchDocumentContent = async () => { + setLoading(true); + try { + const response = await get(`/knowledge-bases/${knowledgeBaseId}/document_content/`, { + params: { document_id: documentId }, + }); + + // 检查API响应格式,提取内容 + if (response.code === 200 && response.data) { + setDocumentContent(response.data); + } else if (response.data && response.data.code === 200) { + // 如果数据包装在data属性中 + setDocumentContent(response.data.data); + } else { + // 兜底情况 + setDocumentContent(response); + } + console.log(documentContent); + + } catch (error) { + console.error('获取文档内容失败:', error); + dispatch( + showNotification({ + type: 'danger', + message: '获取文档内容失败', + }) + ); + } finally { + setLoading(false); + } + }; + + if (!show) return null; + + return ( +
+
+
+
{documentContent?.document_info?.name || '文档预览'}
+ +
+
+ {loading ? ( +
+
+ 加载中... +
+
+ ) : documentContent ? ( +
+ {documentContent?.paragraphs.length > 0 && + documentContent.paragraphs.map((section, index) => { + let contentDisplay; + try { + // 尝试解析JSON内容 + const parsedContent = JSON.parse(section.content); + contentDisplay = ( +
+                                                {JSON.stringify(parsedContent, null, 2)}
+                                            
+ ); + } catch (e) { + // 如果不是JSON,则直接显示文本内容 + contentDisplay =

{section.content}

; + } + + return ( +
+ {contentDisplay} +
+ ); + })} +
+ ) : ( +
+

无法获取文档内容

+
+ )} +
+
+ +
+
+
+ ); +}; + +export default DocumentPreviewModal; diff --git a/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx new file mode 100644 index 0000000..301526c --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/FileUploadModal.jsx @@ -0,0 +1,293 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { uploadDocument, getKnowledgeBaseDocuments } from '../../../../store/knowledgeBase/knowledgeBase.thunks'; + +/** + * 文件上传模态框组件 + */ +const FileUploadModal = ({ show, knowledgeBaseId, onClose }) => { + const dispatch = useDispatch(); + const fileInputRef = useRef(null); + const modalRef = useRef(null); + const [selectedFiles, setSelectedFiles] = useState([]); + const [isUploading, setIsUploading] = useState(false); + const [fileError, setFileError] = useState(''); + const [uploadResults, setUploadResults] = useState(null); + + // 处理上传区域点击事件 + const handleUploadAreaClick = () => { + fileInputRef.current?.click(); + }; + + // 处理拖拽事件 + const handleDragOver = (e) => { + e.preventDefault(); + e.stopPropagation(); + }; + + const handleDrop = (e) => { + e.preventDefault(); + e.stopPropagation(); + + if (e.dataTransfer.files && e.dataTransfer.files.length > 0) { + handleFilesSelected(e.dataTransfer.files); + } + }; + + const handleFileChange = (e) => { + if (e.target.files && e.target.files.length > 0) { + handleFilesSelected(e.target.files); + } + }; + + const handleFilesSelected = (files) => { + setFileError(''); + // 将FileList转为数组 + const filesArray = Array.from(files); + setSelectedFiles((prev) => [...prev, ...filesArray]); + }; + + const removeFile = (index) => { + setSelectedFiles((prev) => prev.filter((_, i) => i !== index)); + }; + + const resetFileInput = () => { + setSelectedFiles([]); + setFileError(''); + setUploadResults(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + const handleUpload = async () => { + if (selectedFiles.length === 0) { + setFileError('请至少选择一个要上传的文件'); + return; + } + + setIsUploading(true); + setUploadResults(null); + + try { + const result = await dispatch( + uploadDocument({ + knowledge_base_id: knowledgeBaseId, + files: selectedFiles, + }) + ).unwrap(); + + // 成功上传后刷新文档列表 + dispatch(getKnowledgeBaseDocuments({ knowledge_base_id: knowledgeBaseId })); + + // 显示上传结果 + setUploadResults(result); + + // 如果没有失败的文件,就在3秒后自动关闭模态窗 + if (result.failed_count === 0) { + setTimeout(() => { + handleClose(); + }, 3000); + } + } catch (error) { + console.error('Upload failed:', error); + setFileError('文件上传失败: ' + (error?.message || '未知错误')); + } finally { + setIsUploading(false); + } + }; + + const handleClose = () => { + // 只有在非上传状态才允许关闭 + if (!isUploading) { + resetFileInput(); + onClose(); + } + }; + + // 清理函数 + useEffect(() => { + return () => { + // 确保在组件卸载时清理所有引用 + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + if (modalRef.current) { + modalRef.current = null; + } + }; + }, []); + + if (!show) return null; + + return ( +
+
+
+
上传文档
+ +
+
+
+ +
+

点击或拖拽文件到此处上传

+

支持 PDF, Word, Excel, TXT, Markdown, CSV 等格式

+
+ {fileError &&
{fileError}
} +
+ + {/* 选择的文件列表 */} + {selectedFiles.length > 0 && ( +
+

已选择 {selectedFiles.length} 个文件:

+
    + {selectedFiles.map((file, index) => ( +
  • +
    +
    {file.name}
    +
    + ({(file.size / 1024).toFixed(0)} KB) +
    +
    + {!isUploading && ( + + )} +
  • + ))} +
+
+ )} + + {/* 上传结果显示 */} + {uploadResults && ( +
+
上传结果
+

+ 总文件: {uploadResults.total_files}, 成功: {uploadResults.uploaded_count}, 失败:{' '} + {uploadResults.failed_count} +

+ + {uploadResults.documents && uploadResults.documents.length > 0 && ( + <> +

上传成功:

+
    + {uploadResults.documents.map((doc) => ( +
  • + + {doc.name} +
  • + ))} +
+ + )} + + {uploadResults.failed_documents && uploadResults.failed_documents.length > 0 && ( + <> +

上传失败:

+
    + {uploadResults.failed_documents.map((doc, index) => ( +
  • + +
    + {doc.name} + {doc.reason && ( + ({doc.reason}) + )} +
    +
  • + ))} +
+ + )} +
+ )} +
+
+ + +
+
+
+ ); +}; + +export default FileUploadModal; diff --git a/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx new file mode 100644 index 0000000..05edc08 --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/KnowledgeBaseForm.jsx @@ -0,0 +1,283 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; + +/** + * 知识库表单组件 + */ +const KnowledgeBaseForm = ({ + formData, + formErrors, + isSubmitting, + onInputChange, + onSubmit, + onDelete, + onTypeChange, + isAdmin, + departments, + availableGroups, +}) => { + // 获取当前用户信息 + const currentUser = useSelector((state) => state.auth.user); + + // 根据用户角色确定可以设置的知识库类型 + const isLeader = currentUser?.role === 'leader'; + + // 获取当前用户可以设置的知识库类型 + const getAvailableTypes = () => { + if (isAdmin) { + return [ + { value: 'admin', label: '公共知识库' }, + { value: 'leader', label: '组长级知识库' }, + { value: 'member', label: '组内知识库' }, + { value: 'private', label: '私有知识库' }, + { value: 'secret', label: '私密知识库' }, + ]; + } else if (isLeader) { + return [ + { value: 'admin', label: '公共知识库' }, + { value: 'member', label: '组内知识库' }, + { value: 'private', label: '私有知识库' }, + ]; + } else { + return [ + { value: 'admin', label: '公共知识库' }, + { value: 'private', label: '私有知识库' }, + ]; + } + }; + + const availableTypes = getAvailableTypes(); + + // 检查类型是否被更改 + const hasTypeChanged = formData.original_type && formData.original_type !== formData.type; + + // 检查部门或组别是否被更改 + const hasDepartmentOrGroupChanged = + (formData.original_department && formData.department !== formData.original_department) || + (formData.original_group && formData.group !== formData.original_group); + + // 是否显示类型更改按钮 + const showTypeChangeButton = hasTypeChanged || (isAdmin && hasDepartmentOrGroupChanged); + + return ( +
+
+
知识库设置
+ +
+
+ + + {formErrors.name &&
{formErrors.name}
} +
+ +
+ + + {formErrors.desc &&
{formErrors.desc}
} +
+ +
+ +
+ {availableTypes.map((type) => ( +
+ + +
+ ))} +
+ {currentUser?.role === 'member' && ( + 您可以修改知识库类型为公共或私有。 + )} + {formErrors.type &&
{formErrors.type}
} +
+ + {/* 仅当不是私有知识库时才显示部门选项 */} + {(formData.type === 'member' || formData.type === 'leader') && ( +
+ + {isAdmin ? ( + <> + + {formErrors.department && ( +
{formErrors.department}
+ )} + + ) : ( + <> + + + )} +
+ )} + + {/* 仅当不是私有知识库时才显示组别选项 */} + {formData.type === 'member' && ( +
+ + {isAdmin ? ( + <> + + {formErrors.group &&
{formErrors.group}
} + {!formData.department && ( + 请先选择部门 + )} + + ) : ( + <> + + + )} +
+ )} + + {/* 类型更改按钮 */} + {showTypeChangeButton && ( +
+
+ {hasTypeChanged && ( +

+ 知识库类型更改为 {formData.type} +

+ )} + {isAdmin && hasDepartmentOrGroupChanged &&

部门/组别已更改

} + 点击更新按钮单独保存这些更改 +
+ +
+ )} + +
+ + +
+
+
+
+ ); +}; + +export default KnowledgeBaseForm; diff --git a/src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx b/src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx new file mode 100644 index 0000000..afcc39b --- /dev/null +++ b/src/pages/KnowledgeBase/Detail/components/UserPermissionsManager.jsx @@ -0,0 +1,789 @@ +import React, { useState, useRef, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { showNotification } from '../../../../store/notification.slice'; +import SvgIcon from '../../../../components/SvgIcon'; + +/** + * 用户权限管理组件 + */ +const UserPermissionsManager = ({ knowledgeBase }) => { + const dispatch = useDispatch(); + + // State for pagination + const [currentPage, setCurrentPage] = useState(1); + const usersPerPage = 10; + + // State for edit modal + const [showEditModal, setShowEditModal] = useState(false); + const [editUser, setEditUser] = useState(null); + + // State for add user modal + const [showAddUserModal, setShowAddUserModal] = useState(false); + const [newUser, setNewUser] = useState({ + username: '', + email: '', + permissionType: '只读', + accessDuration: '一个月', + }); + + // State for batch operations + const [selectedUsers, setSelectedUsers] = useState([]); + const [selectAll, setSelectAll] = useState(false); + const [showBatchDropdown, setShowBatchDropdown] = useState(false); + const batchDropdownRef = useRef(null); + + // State for batch edit modal + const [showBatchEditModal, setShowBatchEditModal] = useState(false); + const [batchEditData, setBatchEditData] = useState({ + permissionType: '只读', + accessDuration: '一个月', + }); + + // Form errors + const [formErrors, setFormErrors] = useState({}); + + // Mock data for users with permissions + const [users, setUsers] = useState([ + { + id: '1001', + username: '张三', + email: 'zhang@abc.com', + permissionType: '只读', + accessDuration: '一个月', + }, + { + id: '1002', + username: '李四', + email: 'li@abc.com', + permissionType: '完全访问', + accessDuration: '永久', + }, + { + id: '1003', + username: '王五', + email: 'wang@abc.com', + permissionType: '只读', + accessDuration: '三个月', + }, + { + id: '1004', + username: '赵六', + email: 'zhao@abc.com', + permissionType: '完全访问', + accessDuration: '六个月', + }, + { + id: '1005', + username: '钱七', + email: 'qian@abc.com', + permissionType: '只读', + accessDuration: '一周', + }, + { + id: '1006', + username: '孙八', + email: 'sun@abc.com', + permissionType: '只读', + accessDuration: '一个月', + }, + { + id: '1007', + username: '周九', + email: 'zhou@abc.com', + permissionType: '完全访问', + accessDuration: '永久', + }, + { + id: '1008', + username: '吴十', + email: 'wu@abc.com', + permissionType: '只读', + accessDuration: '三个月', + }, + ]); + + // Get current users for pagination + const indexOfLastUser = currentPage * usersPerPage; + const indexOfFirstUser = indexOfLastUser - usersPerPage; + const currentUsers = users.slice(indexOfFirstUser, indexOfLastUser); + const totalPages = Math.ceil(users.length / usersPerPage); + + // Handle click outside batch dropdown + useEffect(() => { + function handleClickOutside(event) { + if (batchDropdownRef.current && !batchDropdownRef.current.contains(event.target)) { + setShowBatchDropdown(false); + } + } + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); + + // Handle edit user permissions + const handleEditUser = (user) => { + setEditUser({ ...user }); + setFormErrors({}); + setShowEditModal(true); + }; + + // Handle input change in edit modal + const handleEditInputChange = (e) => { + const { name, value } = e.target; + setEditUser((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error if exists + if (formErrors[name]) { + setFormErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + // Handle input change in add user modal + const handleAddUserInputChange = (e) => { + const { name, value } = e.target; + setNewUser((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error if exists + if (formErrors[name]) { + setFormErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + // Handle input change in batch edit modal + const handleBatchEditInputChange = (e) => { + const { name, value } = e.target; + setBatchEditData((prev) => ({ + ...prev, + [name]: value, + })); + }; + + // Validate edit form + const validateEditForm = () => { + const errors = {}; + + if (!editUser.username.trim()) { + errors.username = '请输入用户名'; + } + + if (!editUser.email.trim()) { + errors.email = '请输入邮箱'; + } else if (!/\S+@\S+\.\S+/.test(editUser.email)) { + errors.email = '请输入有效的邮箱地址'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Validate add user form + const validateAddUserForm = () => { + const errors = {}; + + if (!newUser.username.trim()) { + errors.username = '请输入用户名'; + } + + if (!newUser.email.trim()) { + errors.email = '请输入邮箱'; + } else if (!/\S+@\S+\.\S+/.test(newUser.email)) { + errors.email = '请输入有效的邮箱地址'; + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + // Handle save user permissions + const handleSaveUserPermissions = () => { + // Validate form + if (!validateEditForm()) { + return; + } + + // Here you would typically call an API to update the user permissions + console.log('Updating user permissions:', editUser); + + // Update the users array with the edited user + setUsers((prevUsers) => prevUsers.map((user) => (user.id === editUser.id ? { ...editUser } : user))); + + // Show success notification + dispatch(showNotification({ message: '用户权限已更新', type: 'success' })); + + // Close modal + setShowEditModal(false); + }; + + // Handle add new user + const handleAddUser = () => { + // Validate form + if (!validateAddUserForm()) { + return; + } + + // Generate a new ID + const newId = `user-${Date.now()}`; + + // Create new user object + const userToAdd = { + id: newId, + username: newUser.username, + email: newUser.email, + permissionType: newUser.permissionType, + accessDuration: newUser.accessDuration, + }; + + // Add new user to the users array + setUsers((prevUsers) => [...prevUsers, userToAdd]); + + // Show success notification + dispatch(showNotification({ message: '用户已添加', type: 'success' })); + + // Reset form and close modal + setNewUser({ + username: '', + email: '', + permissionType: '只读', + accessDuration: '一个月', + }); + setShowAddUserModal(false); + }; + + // Handle delete user + const handleDeleteUser = (userId) => { + // Here you would typically call an API to delete the user + console.log('Deleting user:', userId); + + // Remove user from the users array + setUsers((prevUsers) => prevUsers.filter((user) => user.id !== userId)); + + // Remove from selected users if present + setSelectedUsers((prev) => prev.filter((id) => id !== userId)); + + // Show success notification + dispatch(showNotification({ message: '用户已删除', type: 'success' })); + }; + + // Handle batch delete + const handleBatchDelete = () => { + if (selectedUsers.length === 0) return; + + // Here you would typically call an API to delete the selected users + console.log('Batch deleting users:', selectedUsers); + + // Remove selected users from the users array + setUsers((prevUsers) => prevUsers.filter((user) => !selectedUsers.includes(user.id))); + + // Reset selection + setSelectedUsers([]); + setSelectAll(false); + setShowBatchDropdown(false); + + // Show success notification + dispatch(showNotification({ message: `已删除 ${selectedUsers.length} 个用户`, type: 'success' })); + }; + + // Handle batch edit + const handleBatchEdit = () => { + if (selectedUsers.length === 0) return; + + // Here you would typically call an API to update the selected users + console.log('Batch editing users:', selectedUsers, 'with data:', batchEditData); + + // Update selected users in the users array + setUsers((prevUsers) => + prevUsers.map((user) => (selectedUsers.includes(user.id) ? { ...user, ...batchEditData } : user)) + ); + + // Close modal + setShowBatchEditModal(false); + setShowBatchDropdown(false); + + // Show success notification + dispatch(showNotification({ message: `已更新 ${selectedUsers.length} 个用户的权限`, type: 'success' })); + }; + + // Handle select all checkbox + const handleSelectAll = () => { + if (selectAll) { + setSelectedUsers([]); + } else { + setSelectedUsers(currentUsers.map((user) => user.id)); + } + setSelectAll(!selectAll); + }; + + // Handle individual user selection + const handleSelectUser = (userId) => { + if (selectedUsers.includes(userId)) { + setSelectedUsers(selectedUsers.filter((id) => id !== userId)); + setSelectAll(false); + } else { + setSelectedUsers([...selectedUsers, userId]); + // Check if all current page users are now selected + if (selectedUsers.length + 1 === currentUsers.length) { + setSelectAll(true); + } + } + }; + + // Handle page change + const handlePageChange = (pageNumber) => { + setCurrentPage(pageNumber); + // Reset selection when changing pages + setSelectedUsers([]); + setSelectAll(false); + }; + + return ( +
+
+
+
用户权限管理
+
+ {selectedUsers.length > 0 && ( +
+ + {showBatchDropdown && ( +
    +
  • + +
  • +
  • + +
  • +
+ )} +
+ )} + +
+
+ +
+ + + + + + + + + + + + + {currentUsers.length > 0 ? ( + currentUsers.map((user) => ( + + + + + + + + + )) + ) : ( + + + + )} + +
+
+ +
+
用户名邮箱权限类型访问期限操作
+
+ handleSelectUser(user.id)} + /> +
+
{user.username}{user.email} + + {user.permissionType} + + {user.accessDuration} +
+ + +
+
+ 暂无用户 +
+
+ + {/* Pagination */} + {totalPages > 1 && ( + + )} + + {/* Edit User Modal */} + {showEditModal && ( +
+
+
+
+
编辑用户权限
+ +
+
+
+
+ + + {formErrors.username && ( +
{formErrors.username}
+ )} +
+
+ + + {formErrors.email && ( +
{formErrors.email}
+ )} +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ )} + + {/* Add User Modal */} + {showAddUserModal && ( +
+
+
+
+
添加用户
+ +
+
+
+
+ + + {formErrors.username && ( +
{formErrors.username}
+ )} +
+
+ + + {formErrors.email && ( +
{formErrors.email}
+ )} +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ )} + + {/* Batch Edit Modal */} + {showBatchEditModal && ( +
+
+
+
+
批量修改权限
+ +
+
+

+ 您正在修改 {selectedUsers.length} 个用户的权限 +

+
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+ )} + + {/* Modal backdrop */} + {(showEditModal || showAddUserModal || showBatchEditModal) && ( +
+ )} +
+
+ ); +}; + +export default UserPermissionsManager; diff --git a/src/pages/KnowledgeBase/KnowledgeBase.css b/src/pages/KnowledgeBase/KnowledgeBase.css new file mode 100644 index 0000000..785612f --- /dev/null +++ b/src/pages/KnowledgeBase/KnowledgeBase.css @@ -0,0 +1,53 @@ +.knowledge-base-page { + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +.knowledge-base-header { + background-color: #f8f9fa; + border-radius: 10px; + margin-bottom: 20px; +} + +.knowledge-base-cards-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; + margin-top: 20px; +} + +.search-bar-container { + position: relative; + flex: 1; + max-width: 500px; +} + +.search-results-dropdown { + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + border-radius: 8px; + z-index: 1050; +} + +.search-result-item { + transition: background-color 0.2s; +} + +.search-result-item:hover { + background-color: #f8f9fa; +} + +.hover-bg-light:hover { + background-color: #f8f9fa; +} + +/* 响应式样式 */ +@media (max-width: 768px) { + .knowledge-base-cards-container { + grid-template-columns: 1fr; + } + + .search-bar-container { + max-width: 100%; + } +} \ No newline at end of file diff --git a/src/pages/KnowledgeBase/KnowledgeBase.jsx b/src/pages/KnowledgeBase/KnowledgeBase.jsx new file mode 100644 index 0000000..4f4a15b --- /dev/null +++ b/src/pages/KnowledgeBase/KnowledgeBase.jsx @@ -0,0 +1,538 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { useDispatch, useSelector } from 'react-redux'; +import { showNotification } from '../../store/notification.slice'; +import { + fetchKnowledgeBases, + searchKnowledgeBases, + createKnowledgeBase, + deleteKnowledgeBase, + requestKnowledgeBaseAccess, +} from '../../store/knowledgeBase/knowledgeBase.thunks'; +import { clearSearchResults } from '../../store/knowledgeBase/knowledgeBase.slice'; +import SvgIcon from '../../components/SvgIcon'; +import AccessRequestModal from '../../components/AccessRequestModal'; +import CreateKnowledgeBaseModal from '../../components/CreateKnowledgeBaseModal'; +import Pagination from '../../components/Pagination'; +import SearchBar from '../../components/SearchBar'; +import ApiModeSwitch from '../../components/ApiModeSwitch'; + +// 导入拆分的组件 +import KnowledgeBaseList from './components/KnowledgeBaseList'; + +export default function KnowledgeBase() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [showCreateModal, setShowCreateModal] = useState(false); + const [showAccessRequestModal, setShowAccessRequestModal] = useState(false); + const [formErrors, setFormErrors] = useState({}); + const [accessRequestKnowledgeBase, setAccessRequestKnowledgeBase] = useState({ + id: '', + title: '', + }); + const [isSubmittingRequest, setIsSubmittingRequest] = useState(false); + const [createdKnowledgeBaseId, setCreatedKnowledgeBaseId] = useState(null); + + // 获取当前用户信息 + const currentUser = useSelector((state) => state.auth.user); + + const [newKnowledgeBase, setNewKnowledgeBase] = useState({ + name: '', + desc: '', + type: 'private', // 默认为私有知识库 + department: currentUser?.department || '', + group: currentUser?.group || '', + }); + + // Search state + const [searchKeyword, setSearchKeyword] = useState(''); + const [isSearchDropdownOpen, setIsSearchDropdownOpen] = useState(false); + + // Pagination state + const [pagination, setPagination] = useState({ + page: 1, + page_size: 10, + }); + + // Get knowledge bases from Redux store + // 更新为新的Redux状态结构 + const knowledgeBases = useSelector((state) => state.knowledgeBase.knowledgeBases); + const loading = useSelector((state) => state.knowledgeBase.loading); + const paginationData = useSelector((state) => state.knowledgeBase.pagination); + const error = useSelector((state) => state.knowledgeBase.error); + const operationStatus = useSelector((state) => state.knowledgeBase.editStatus); + const operationError = useSelector((state) => state.knowledgeBase.error); + + // 从Redux获取搜索结果和加载状态 + const searchResults = useSelector((state) => state.knowledgeBase.searchResults); + const searchLoading = useSelector((state) => state.knowledgeBase.searchLoading); + + // Fetch knowledge bases when component mounts or pagination changes + useEffect(() => { + // 无论是否在搜索,都正常获取知识库列表 + dispatch(fetchKnowledgeBases(pagination)); + }, [dispatch, pagination.page, pagination.page_size]); + + // Show loading state while fetching data + const isLoading = loading; + + // Show error notification if fetch fails + useEffect(() => { + if (!isLoading && error) { + dispatch( + showNotification({ + message: `获取知识库列表失败: ${error.message || error}`, + type: 'danger', + }) + ); + } + }, [isLoading, error, dispatch]); + + // Show notification for operation status + useEffect(() => { + if (operationStatus === 'successful') { + // 操作成功通知由具体函数处理,这里只刷新列表 + + // Refresh the list after successful operation + dispatch(fetchKnowledgeBases(pagination)); + } else if (operationStatus === 'failed' && operationError) { + dispatch( + showNotification({ + message: `操作失败: ${operationError.message || operationError}`, + type: 'danger', + }) + ); + } + }, [operationStatus, operationError, dispatch, pagination]); + + // Handle search input change + const handleSearchInputChange = (e) => { + const value = e.target.value; + console.log('搜索框输入值:', value); + setSearchKeyword(value); + + // 如果搜索框清空,关闭下拉框 + if (!value.trim()) { + dispatch(clearSearchResults()); + setIsSearchDropdownOpen(false); + } + }; + + // Handle search submit - 只影响下拉框,不影响主列表 + const handleSearch = (e) => { + e.preventDefault(); + + if (searchKeyword.trim()) { + // 只设置下拉框搜索状态,不设置全局isSearching状态 + dispatch( + searchKnowledgeBases({ + keyword: searchKeyword, + page: 1, + page_size: 5, // 下拉框只显示少量结果 + }) + ); + setIsSearchDropdownOpen(true); + } else { + // 清空搜索及关闭下拉框 + handleClearSearch(); + } + }; + + // Handle clear search + const handleClearSearch = () => { + setSearchKeyword(''); + // 不影响主列表显示,只关闭下拉框 + setIsSearchDropdownOpen(false); + dispatch(clearSearchResults()); + }; + + // Handle pagination change + const handlePageChange = (newPage) => { + setPagination((prev) => ({ + ...prev, + page: newPage, + })); + }; + + // Handle page size change + const handlePageSizeChange = (newPageSize) => { + setPagination({ + page: 1, // Reset to first page when changing page size + page_size: newPageSize, + }); + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + const isAdmin = currentUser?.role === 'admin'; + const isLeader = currentUser?.role === 'leader'; + + // 根据用户角色允许修改部门和组别字段 + if (name === 'department' || name === 'group') { + // 仅管理员可以修改部门 + if (name === 'department' && !isAdmin) { + return; + } + + // 仅管理员和组长可以修改组别 + if (name === 'group' && !isAdmin && !isLeader) { + return; + } + + // 更新字段值 + setNewKnowledgeBase((prev) => ({ + ...prev, + [name]: value, + })); + + // 如果更新部门,重置组别 + if (name === 'department') { + setNewKnowledgeBase((prev) => ({ + ...prev, + group: '', // 部门变更时重置组别 + })); + } + + return; + } + + // 检查用户是否有权限选择指定的知识库类型 + if (name === 'type') { + const role = currentUser?.role; + let allowed = false; + + // 根据角色判断可以选择的知识库类型 + if (role === 'admin') { + // 管理员可以选择任何类型 + allowed = ['admin', 'leader', 'member', 'private', 'secret'].includes(value); + } else if (role === 'leader') { + // 组长只能选择 member 和 private + allowed = ['admin', 'member', 'private'].includes(value); + } else { + // 普通成员只能选择 private + allowed = ['admin', 'private'].includes(value); + } + + if (!allowed) { + dispatch( + showNotification({ + message: '您没有权限创建此类型的知识库', + type: 'warning', + }) + ); + return; + } + } + + setNewKnowledgeBase((prev) => ({ + ...prev, + [name]: value, + })); + + // Clear error when user types + if (formErrors[name]) { + setFormErrors((prev) => ({ + ...prev, + [name]: '', + })); + } + }; + + const validateCreateForm = () => { + const errors = {}; + const isAdmin = currentUser?.role === 'admin'; + const isLeader = currentUser?.role === 'leader'; + // 组长级和member类型知识库需要选择组别,私有知识库不需要 + const needDepartmentAndGroup = newKnowledgeBase.type === 'member' || newKnowledgeBase.type === 'leader'; + // 私有知识库不需要选择部门和组别 + const isPrivate = newKnowledgeBase.type === 'private'; + + if (!newKnowledgeBase.name.trim()) { + errors.name = '请输入知识库名称'; + } + + if (!newKnowledgeBase.desc.trim()) { + errors.desc = '请输入知识库描述'; + } + + if (!newKnowledgeBase.type) { + errors.type = '请选择知识库类型'; + } + + // 对于member和leader级别的知识库,检查是否选择了部门和组别 + if (needDepartmentAndGroup && !isPrivate) { + // 管理员必须选择部门 + if (isAdmin && !newKnowledgeBase.department) { + errors.department = `创建${newKnowledgeBase.type === 'leader' ? '组长级' : '组内'}知识库时必须选择部门`; + } + + // 所有用户创建member和leader级别知识库时必须选择组别 + if (!newKnowledgeBase.group) { + errors.group = `创建${newKnowledgeBase.type === 'leader' ? '组长级' : '组内'}知识库时必须选择组别`; + } + } + + setFormErrors(errors); + return Object.keys(errors).length === 0; + }; + + const handleCreateKnowledgeBase = async () => { + // Validate form + if (!validateCreateForm()) { + return; + } + + try { + // 私有知识库不需要部门和组别信息 + const isPrivate = newKnowledgeBase.type === 'private'; + + // Dispatch create knowledge base action + const resultAction = await dispatch( + createKnowledgeBase({ + name: newKnowledgeBase.name, + desc: newKnowledgeBase.desc, + description: newKnowledgeBase.desc, + type: newKnowledgeBase.type, + department: !isPrivate ? newKnowledgeBase.department : '', + group: !isPrivate ? newKnowledgeBase.group : '', + }) + ); + + console.log('创建知识库返回数据:', resultAction); + + // Check if the action was successful + if (createKnowledgeBase.fulfilled.match(resultAction)) { + console.log('创建成功,payload:', resultAction.payload); + const { knowledge_base } = resultAction.payload; + const { id } = knowledge_base; + // Get ID from payload and navigate + if (id) { + console.log('新知识库ID:', id); + + // 显示成功通知 + dispatch( + showNotification({ + message: '知识库创建成功', + type: 'success', + }) + ); + + // 直接导航到新创建的知识库详情页 + navigate(`/knowledge-base/${id}`); + } else { + console.error('无法获取新知识库ID:', resultAction.payload); + dispatch( + showNotification({ + message: '创建成功,但无法获取知识库ID', + type: 'warning', + }) + ); + } + } else { + console.error('创建知识库失败:', resultAction.error); + dispatch( + showNotification({ + message: `创建知识库失败: ${resultAction.error?.message || '未知错误'}`, + type: 'danger', + }) + ); + } + } catch (error) { + console.error('创建知识库出错:', error); + dispatch( + showNotification({ + message: `创建知识库出错: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + } + + // Reset form and close modal + setNewKnowledgeBase({ name: '', desc: '', type: 'private', department: '', group: '' }); + setFormErrors({}); + setShowCreateModal(false); + }; + + // Handle card click to navigate to knowledge base detail + const handleCardClick = (id, permissions) => { + // 检查用户是否有读取权限 + if (!permissions || permissions.can_read === false) { + dispatch( + showNotification({ + message: '您没有访问此知识库的权限,请先申请权限', + type: 'warning', + }) + ); + return; + } + + // 有权限则跳转到详情页 + navigate(`/knowledge-base/${id}/datasets`); + }; + + const handleRequestAccess = (id, title) => { + setAccessRequestKnowledgeBase({ + id, + title, + }); + setShowAccessRequestModal(true); + }; + + const handleSubmitAccessRequest = async (requestData) => { + setIsSubmittingRequest(true); + + try { + // 使用权限服务发送请求 - 通过dispatch调用thunk + await dispatch(requestKnowledgeBaseAccess(requestData)).unwrap(); + + // Close modal after success + setShowAccessRequestModal(false); + } catch (error) { + dispatch( + showNotification({ + message: `权限申请失败: ${error.response?.data?.message || '请稍后重试'}`, + type: 'danger', + }) + ); + } finally { + setIsSubmittingRequest(false); + } + }; + + const handleDelete = (e, id) => { + e.preventDefault(); + e.stopPropagation(); + + // Dispatch delete knowledge base action + dispatch(deleteKnowledgeBase(id)) + .unwrap() + .then(() => { + dispatch( + showNotification({ + message: '知识库已删除', + type: 'success', + }) + ); + // 不需要重新获取知识库列表,Redux store 已经在 reducer 中更新 + }) + .catch((error) => { + dispatch( + showNotification({ + message: `删除失败: ${error || '未知错误'}`, + type: 'danger', + }) + ); + }); + }; + + // Calculate total pages + const totalPages = Math.ceil(paginationData.total / pagination.page_size); + + // 打开创建知识库弹窗 + const handleOpenCreateModal = () => { + const isAdmin = currentUser?.role === 'admin'; + const isLeader = currentUser?.role === 'leader'; + + // 默认知识库类型设为公共知识库 + let defaultType = 'admin'; + + // 初始部门和组别 + let department = currentUser?.department || ''; + let group = currentUser?.group || ''; + + setNewKnowledgeBase({ + name: '', + desc: '', + type: defaultType, + department: department, + group: group, + }); + + setFormErrors({}); + setShowCreateModal(true); + }; + + // 处理点击搜索结果 + const handleSearchResultClick = (id, permissions) => { + if (permissions?.can_read) { + navigate(`/knowledge-base/${id}/datasets`); + } + }; + + return ( +
+ {/*
+ +
*/} +
+ + +
+ + {isLoading ? ( +
+
+ 加载中... +
+
+ ) : ( + <> + + + {/* Pagination - 始终显示 */} + {totalPages > 1 && ( + + )} + + )} + + {/* 新建知识库弹窗 */} + setShowCreateModal(false)} + onChange={handleInputChange} + onSubmit={handleCreateKnowledgeBase} + currentUser={currentUser} + /> + + {/* 申请权限弹窗 */} + setShowAccessRequestModal(false)} + onSubmit={handleSubmitAccessRequest} + isSubmitting={isSubmittingRequest} + /> +
+ ); +} diff --git a/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx new file mode 100644 index 0000000..4b51caf --- /dev/null +++ b/src/pages/KnowledgeBase/components/KnowledgeBaseList.jsx @@ -0,0 +1,41 @@ +import React from 'react'; +import KnowledgeCard from './KnowledgeCard'; + +/** + * 知识库列表组件 + */ +const KnowledgeBaseList = ({ knowledgeBases, isSearching, onCardClick, onRequestAccess, onDelete }) => { + if (!knowledgeBases?.length) { + return ( +
+ {isSearching ? '没有找到匹配的知识库' : '暂无知识库,请创建新的知识库'} +
+ ); + } + + return ( +
+ {knowledgeBases.map((item) => ( + + onCardClick(item.id, item.permissions)} + onRequestAccess={onRequestAccess} + onDelete={(e) => onDelete(e, item.id)} + type={item.type} + department={item.department} + group={item.group} + /> + + ))} +
+ ); +}; + +export default KnowledgeBaseList; diff --git a/src/pages/KnowledgeBase/components/KnowledgeCard.jsx b/src/pages/KnowledgeBase/components/KnowledgeCard.jsx new file mode 100644 index 0000000..9649023 --- /dev/null +++ b/src/pages/KnowledgeBase/components/KnowledgeCard.jsx @@ -0,0 +1,121 @@ +import React from 'react'; +import SvgIcon from '../../../components/SvgIcon'; +import { useNavigate } from 'react-router-dom'; + +export default function KnowledgeCard({ + id, + title, + description, + documents, + date, + access, + permissions, + onClick, + onRequestAccess, + onDelete, + type, + department, + group, +}) { + const navigate = useNavigate(); + + const handleNewChat = (e) => { + e.preventDefault(); + e.stopPropagation(); + navigate(`/chat/${id}`); + }; + + const handleRequestAccess = (e) => { + e.preventDefault(); + e.stopPropagation(); + onRequestAccess(id, title); + }; + + // 自定义样式,限制文本最多显示两行 + const descriptionStyle = { + display: '-webkit-box', + WebkitLineClamp: 2, + WebkitBoxOrient: 'vertical', + overflow: 'hidden', + textOverflow: 'ellipsis', + minHeight: '3rem', // 保持一致的高度,即使描述很短 + }; + + return ( +
+
+
+ {permissions && permissions.can_delete && ( +
+ +
    +
  • + 删除 + +
  • +
+
+ )} +
+ {department || ''} {group || 'N/A'} +
+

+ {description} +

+
+ + {documents} 文档 + + + {date} + +
+ {/*
+ + {type === 'private' ? '私有' : '公开'} + + {department && {department}} + {group && {group}} +
*/} +
+ {access === 'full' ? ( + + + 完全访问 + + ) : access === 'read' ? ( + + + 只读访问 + + ) : ( + + + 无访问权限 + + )} + {access === 'full' || access === 'read' ? ( + <> + ) : ( + // + + )} +
+
+
+ ); +} diff --git a/src/pages/Permissions/Permissions.css b/src/pages/Permissions/Permissions.css new file mode 100644 index 0000000..5fa7f2d --- /dev/null +++ b/src/pages/Permissions/Permissions.css @@ -0,0 +1,39 @@ +.permissions-container { + padding: 24px; + background-color: #f8f9fa; + min-height: calc(100vh - 64px); +} + +.permissions-section { + background-color: #fff; + border-radius: 8px; + padding: 24px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +.api-mode-control { + background-color: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +.api-mode-control .api-mode-switch { + display: flex; + flex-direction: column; +} + +@media (max-width: 768px) { + .permissions-container { + padding: 16px; + } + + .permissions-section, + .api-mode-control { + padding: 16px; + } +} + +.permissions-section:last-child { + margin-bottom: 0; +} \ No newline at end of file diff --git a/src/pages/Permissions/PermissionsPage.jsx b/src/pages/Permissions/PermissionsPage.jsx new file mode 100644 index 0000000..27f9e19 --- /dev/null +++ b/src/pages/Permissions/PermissionsPage.jsx @@ -0,0 +1,24 @@ +import React, { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import PendingRequests from './components/PendingRequests'; +import UserPermissions from './components/UserPermissions'; +import './Permissions.css'; + +export default function PermissionsPage() { + const { user } = useSelector((state) => state.auth); + + + return ( +
+
+ +
+ {user && user.role === 'admin' && ( +
+ +
+ )} +
+ ); +} diff --git a/src/pages/Permissions/components/PendingRequests.css b/src/pages/Permissions/components/PendingRequests.css new file mode 100644 index 0000000..9ddbd81 --- /dev/null +++ b/src/pages/Permissions/components/PendingRequests.css @@ -0,0 +1,251 @@ +.permission-requests { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.permission-card { + transition: all 0.2s ease; + border-radius: 8px; + background-color: #343a40; +} + +.permission-card:hover { + transform: translateY(-2px); + box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; +} + +.custom-badge { + padding: 0.35em 0.65em; + font-size: 0.85em; +} + +.badge.bg-info { + background-color: #0dcaf0 !important; +} + +.badge.bg-success { + background-color: #198754 !important; +} + +.badge.bg-danger { + background-color: #dc3545 !important; +} + +.badge.bg-secondary { + background-color: #6c757d !important; +} + +/* 表格行鼠标样式 */ +.cursor-pointer { + cursor: pointer; +} + +/* 滑动面板样式 */ +.slide-over-backdrop { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + z-index: 1040; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s ease, visibility 0.3s ease; +} + +.slide-over-backdrop.show { + opacity: 1; + visibility: visible; +} + +.slide-over { + position: fixed; + top: 0; + right: -450px; + width: 450px; + height: 100%; + background-color: #fff; + z-index: 1050; + transition: right 0.3s ease; + box-shadow: -2px 0 10px rgba(0, 0, 0, 0.1); +} + +.slide-over.show { + right: 0; +} + +.slide-over-content { + display: flex; + flex-direction: column; + height: 100%; +} + +.slide-over-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem; + border-bottom: 1px solid #e9ecef; +} + +.slide-over-body { + flex: 1; + padding: 1rem; + overflow-y: auto; +} + +.slide-over-footer { + padding: 1rem; + border-top: 1px solid #e9ecef; + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} + +/* 头像占位符 */ +.avatar-placeholder { + width: 40px; + height: 40px; + border-radius: 50%; + background-color: #6c757d; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 1.2rem; +} + +/* 新增样式 - 白色基调 */ +.badge-count { + background-color: #ff4d4f; + color: white; + border-radius: 20px; + padding: 4px 12px; + font-size: 14px; +} + +.pending-requests-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.pending-request-item { + position: relative; + background-color: #fff; + border-radius: 8px; + padding: 16px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + cursor: pointer; + transition: all 0.2s ease; +} + +.pending-request-item:hover { + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); +} + +.request-header { + display: flex; + justify-content: space-between; + margin-bottom: 12px; +} + +.user-info h6 { + font-weight: 600; +} + +.department { + color: #666; + font-size: 14px; + margin: 0; +} + +.request-date { + color: #999; + font-size: 14px; +} + +.request-content { + color: #333; +} + +.permission-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.permission-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.permission-badge.read { + background-color: #e6f7ff; + color: #1890ff; + border: 1px solid #91d5ff; +} + +.permission-badge.edit { + background-color: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; +} + +.permission-badge.delete { + background-color: #fff2f0; + color: #ff4d4f; + border: 1px solid #ffccc7; +} + +.request-actions { + position: absolute; + right: 1rem; + bottom: 1rem; + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 12px; +} + + +/* 分页控件样式 */ +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; + padding: 10px 0; +} + +.pagination-button { + background-color: #fff; + border: 1px solid #d9d9d9; + color: #333; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.pagination-button:hover:not(:disabled) { + border-color: #1890ff; + color: #1890ff; +} + +.pagination-button:disabled { + color: #d9d9d9; + cursor: not-allowed; +} + +.pagination-info { + margin: 0 15px; + color: #666; + font-size: 14px; +} \ No newline at end of file diff --git a/src/pages/Permissions/components/PendingRequests.jsx b/src/pages/Permissions/components/PendingRequests.jsx new file mode 100644 index 0000000..8639e32 --- /dev/null +++ b/src/pages/Permissions/components/PendingRequests.jsx @@ -0,0 +1,470 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useLocation } from 'react-router-dom'; +import { showNotification } from '../../../store/notification.slice'; +import { + fetchPermissionsThunk, + approvePermissionThunk, + rejectPermissionThunk, +} from '../../../store/permissions/permissions.thunks'; +import { resetOperationStatus } from '../../../store/permissions/permissions.slice'; +import './PendingRequests.css'; // 引入外部CSS文件 +import SvgIcon from '../../../components/SvgIcon'; +import RequestDetailSlideOver from './RequestDetailSlideOver'; + +export default function PendingRequests() { + const dispatch = useDispatch(); + const location = useLocation(); + const [responseMessage, setResponseMessage] = useState(''); + const [showResponseInput, setShowResponseInput] = useState(false); + const [currentRequestId, setCurrentRequestId] = useState(null); + const [isApproving, setIsApproving] = useState(false); + const [selectedRequest, setSelectedRequest] = useState(null); + const [showSlideOver, setShowSlideOver] = useState(false); + + // 从Redux store获取权限申请列表状态 + const { + results: permissionRequests, + status: fetchStatus, + error: fetchError, + page: currentPage, + page_size: pageSize, + total, + } = useSelector((state) => state.permissions.pending); + + // 计算总页数 + const totalPages = Math.ceil(total / pageSize) || 1; + + // 从Redux store获取批准/拒绝操作的状态 + const { + status: approveRejectStatus, + error: approveRejectError, + currentId: processingId, + } = useSelector((state) => state.permissions.approveReject); + + // 获取待处理申请列表 + useEffect(() => { + dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize })); + }, [dispatch, currentPage, pageSize]); + + // 处理从通知中心跳转过来的请求 + useEffect(() => { + if (location.state?.showRequestDetail) { + const { requestId, requestData } = location.state; + setSelectedRequest(requestData); + setShowSlideOver(true); + // 清除 location state + window.history.replaceState({}, document.title); + } + }, [location.state]); + + // 监听批准/拒绝操作的状态变化 + useEffect(() => { + if (approveRejectStatus === 'succeeded') { + dispatch( + showNotification({ + message: isApproving ? '已批准申请' : '已拒绝申请', + type: 'success', + }) + ); + setShowResponseInput(false); + setCurrentRequestId(null); + setResponseMessage(''); + setShowSlideOver(false); + setSelectedRequest(null); + + // 重新获取申请列表 + dispatch(fetchPermissionsThunk({ page: currentPage, page_size: pageSize })); + + // 重置状态 + dispatch(resetOperationStatus()); + } else if (approveRejectStatus === 'failed') { + dispatch( + showNotification({ + message: approveRejectError || '处理申请失败', + type: 'danger', + }) + ); + // 重置状态 + dispatch(resetOperationStatus()); + } + }, [approveRejectStatus, approveRejectError, dispatch, isApproving, currentPage, pageSize]); + + // 打开回复输入框 + const handleOpenResponseInput = (requestId, approving) => { + setCurrentRequestId(requestId); + setIsApproving(approving); + setShowResponseInput(true); + }; + + // 关闭回复输入框 + const handleCloseResponseInput = () => { + setShowResponseInput(false); + setCurrentRequestId(null); + setResponseMessage(''); + }; + + // 处理申请(批准或拒绝) + const handleProcessRequest = () => { + if (!currentRequestId) return; + + const params = { + id: currentRequestId, + responseMessage, + }; + + if (isApproving) { + dispatch(approvePermissionThunk(params)); + } else { + dispatch(rejectPermissionThunk(params)); + } + }; + + // 处理行点击,显示滑动面板 + const handleRowClick = (request) => { + setSelectedRequest(request); + setShowSlideOver(true); + }; + + // 关闭滑动面板 + const handleCloseSlideOver = () => { + setShowSlideOver(false); + setTimeout(() => { + setSelectedRequest(null); + }, 300); // 等待动画结束后再清除选中的申请 + }; + + // 直接处理申请(不显示弹窗) + const handleDirectProcess = (requestId, approve) => { + setCurrentRequestId(requestId); + setIsApproving(approve); + + const params = { + id: requestId, + responseMessage: approve ? '已批准' : '已拒绝', + }; + + if (approve) { + dispatch(approvePermissionThunk(params)); + } else { + dispatch(rejectPermissionThunk(params)); + } + }; + + // 处理页码变化 + const handlePageChange = (page) => { + dispatch(fetchPermissionsThunk({ page, page_size: pageSize })); + }; + + // 渲染分页控件 + const renderPagination = () => { + if (totalPages <= 1) return null; + + return ( +
+ + +
+ {currentPage} / {totalPages} +
+ + +
+ ); + }; + + // 根据状态渲染不同的状态标签 + const renderStatusBadge = (status) => { + switch (status) { + case 'approved': + return ( + + + 已批准 + + ); + case 'rejected': + return ( + + + 已拒绝 + + ); + case 'pending': + return ( + + 待处理 + + ); + default: + return null; + } + }; + + // 获取知识库类型的中文描述 + const getKnowledgeBaseTypeText = (type) => { + switch (type) { + case 'admin': + return '公共知识库'; + case 'member': + return '组内知识库'; + case 'private': + return '私人知识库'; + case 'leader': + return '组长级知识库'; + case 'secret': + return '私密知识库'; + default: + return '未知类型'; + } + }; + + // 渲染加载状态 + if (fetchStatus === 'loading' && permissionRequests.length === 0) { + return ( +
+
+ 加载中... +
+

加载权限申请...

+
+ ); + } + + // 渲染错误状态 + if (fetchStatus === 'failed' && permissionRequests.length === 0) { + return ( +
+ {fetchError || '获取权限申请失败'} +
+ ); + } + + // 渲染空状态 + if (permissionRequests.length === 0) { + return ( +
+ 暂无权限申请记录 +
+ ); + } + + // 渲染申请列表 + return ( + <> +
+
权限申请列表
+
+ {permissionRequests.filter((req) => req.status === 'pending').length}个待处理 +
+
+ +
+ {permissionRequests.map((request) => ( +
handleRowClick(request)}> +
+
+
{request.applicant.name || request.applicant.username}
+ {request.applicant.department || '未分配部门'} +
+
{new Date(request.created_at).toLocaleDateString()}
+
+ +
+
+
+

申请访问:{request.knowledge_base.name}

+ + 类型:{getKnowledgeBaseTypeText(request.knowledge_base.type)} + +
+ {renderStatusBadge(request.status)} +
+ + {request.permissions.can_edit ? ( + + + 完全访问 + + ) : ( + request.permissions.can_read && ( + + + 只读访问 + + ) + )} + + {request.expires_at && ( +
+ + 到期时间: {new Date(request.expires_at).toLocaleDateString()} + +
+ )} + + {request.response_message && ( +
+ 审批意见: {request.response_message} +
+ )} +
+
+ {request.status === 'pending' && request.role === 'approver' ? ( + <> + + + + ) : ( +
+ {request.status === 'approved' + ? '已批准' + : request.status === 'pending' + ? '待处理' + : '已拒绝'} + {request.updated_at && ( + + {new Date(request.updated_at).toLocaleDateString()} + + )} +
+ )} +
+
+ ))} +
+ + {/* 分页控件 */} + {renderPagination()} + + {/* 使用新的滑动面板组件 */} + handleOpenResponseInput(id, true)} + onReject={(id) => handleOpenResponseInput(id, false)} + processingId={processingId} + approveRejectStatus={approveRejectStatus} + isApproving={isApproving} + /> + + {/* 回复输入弹窗 */} + {showResponseInput && ( +
+
+
+
+
{isApproving ? '批准' : '拒绝'}申请
+ +
+
+
+ + +
+
+
+ + +
+
+
+
+
+ )} + + ); +} diff --git a/src/pages/Permissions/components/RequestDetailSlideOver.jsx b/src/pages/Permissions/components/RequestDetailSlideOver.jsx new file mode 100644 index 0000000..8480af8 --- /dev/null +++ b/src/pages/Permissions/components/RequestDetailSlideOver.jsx @@ -0,0 +1,202 @@ +import React from 'react'; +import SvgIcon from '../../../components/SvgIcon'; + +export default function RequestDetailSlideOver({ + show, + onClose, + request, + onApprove, + onReject, + processingId, + approveRejectStatus, + isApproving, +}) { + if (!request) return null; + + // 获取申请人信息 + const applicantName = request.applicant?.name || request.applicant?.username || '未知用户'; + const applicantDept = request.applicant?.department || '未分配部门'; + const applicantInitial = applicantName.charAt(0); + const knowledgeBaseName = request.knowledge_base?.name || '未知知识库'; + const knowledgeBaseId = request.knowledge_base?.id || ''; + const knowledgeBaseType = request.knowledge_base?.type || ''; + + // 获取知识库类型的中文描述 + const getKnowledgeBaseTypeText = (type) => { + switch (type) { + case 'admin': + return '公共知识库'; + case 'member': + return '组内知识库'; + case 'private': + return '私人知识库'; + case 'leader': + return '组长级知识库'; + case 'secret': + return '私密知识库'; + default: + return '未知类型'; + } + }; + + return ( + <> +
+
+
+
+
申请详情
+ +
+
+
+
申请人信息
+
+
{applicantInitial}
+
+
{applicantName}
+
{applicantDept}
+
+
+
+ +
+
知识库信息
+

+ 名称: {knowledgeBaseName} +

+

+ ID: {knowledgeBaseId} +

+ {knowledgeBaseType && ( +

+ 类型: {getKnowledgeBaseTypeText(knowledgeBaseType)} +

+ )} +
+ +
+
申请权限
+
+ {request.permissions?.can_read && !request.permissions?.can_edit && ( + + + 只读 + + )} + {request.permissions?.can_edit && ( + + + 编辑 + + )} + {request.permissions?.can_delete && ( + + + 删除 + + )} +
+
+ +
+
申请时间
+

{new Date(request.created_at || Date.now()).toLocaleString()}

+
+ + {request.expires_at && ( +
+
到期时间
+

{new Date(request.expires_at).toLocaleString()}

+
+ )} + + {request.status && ( +
+
申请状态
+

+ {request.status === 'pending' ? ( + 待处理 + ) : request.status === 'approved' ? ( + 已批准 + ) : ( + 已拒绝 + )} +

+
+ )} + + {request.approver && ( +
+
审批人
+

+ {request.approver.name || request.approver.username} ( + {request.approver.department || '未分配部门'}) +

+
+ )} + + {request.response_message && ( +
+
审批意见
+
{request.response_message}
+
+ )} + +
+
申请理由
+
{request.reason || '无申请理由'}
+
+
+ {request.status === 'pending' && request.role === 'approver' ? ( +
+ + +
+ ) : request.status === 'pending' ? ( +
+
+ 待处理 + 等待审批人处理 +
+
+ ) : null} +
+
+ + ); +} diff --git a/src/pages/Permissions/components/UserPermissionDetails.jsx b/src/pages/Permissions/components/UserPermissionDetails.jsx new file mode 100644 index 0000000..5f314fd --- /dev/null +++ b/src/pages/Permissions/components/UserPermissionDetails.jsx @@ -0,0 +1,300 @@ +import React, { useState, useEffect } from 'react'; +import { get, post } from '../../../services/api'; + +export default function UserPermissionDetails({ user, onClose, onSave }) { + const [updatedPermissions, setUpdatedPermissions] = useState({}); + const [userPermissions, setUserPermissions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [savingPermissions, setSavingPermissions] = useState(false); + const [successMessage, setSuccessMessage] = useState(null); + + useEffect(() => { + // If we already have the permissions data in the user object, use it + if (user.permissions && Array.isArray(user.permissions)) { + setUserPermissions(user.permissions); + setLoading(false); + } + }, [user]); + + const handlePermissionChange = (knowledgeBaseId, newPermissionType) => { + setUpdatedPermissions((prev) => ({ + ...prev, + [knowledgeBaseId]: newPermissionType, + })); + }; + + const handleSave = async () => { + setSavingPermissions(true); + setError(null); + setSuccessMessage(null); + + try { + const permissionUpdates = Object.entries(updatedPermissions).map( + async ([knowledgeBaseId, permissionType]) => { + const permissions = { + can_read: permissionType !== 'none', + can_edit: ['edit', 'admin'].includes(permissionType), + can_delete: permissionType === 'admin', + }; + + const requestBody = { + user_id: user.user_info.id, + knowledge_base_id: knowledgeBaseId, + permissions: permissions, + // Optional expiration date - can be added if needed + // expires_at: "2025-12-31T23:59:59Z" + }; + + try { + const response = await post('/permissions/update_permission/', requestBody); + return response; + } catch (err) { + throw new Error(`更新知识库 ${knowledgeBaseId} 权限失败: ${err.message || '未知错误'}`); + } + } + ); + + await Promise.all(permissionUpdates); + + setSuccessMessage('权限更新成功'); + // Reset updated permissions + setUpdatedPermissions({}); + + // Notify parent component if needed + if (onSave) { + onSave(user.user_info.id); + } + } catch (err) { + setError(err.message || '更新权限时发生错误'); + } finally { + setSavingPermissions(false); + } + }; + + // 获取权限类型的显示文本 + const getPermissionTypeText = (permissionType) => { + switch (permissionType) { + case 'none': + return '无权限'; + case 'read': + return '只读访问'; + case 'edit': + return '编辑权限'; + case 'admin': + return '管理权限'; + default: + return '未知权限'; + } + }; + + // 获取权限类型的值 + const getPermissionType = (permission) => { + if (!permission) return 'none'; + if (permission.can_delete) return 'admin'; + if (permission.can_edit) return 'edit'; + if (permission.can_read) return 'read'; + return 'none'; + }; + + return ( +
+
+
+
+
{user.user_info.name} 的权限详情
+ +
+
+ {successMessage && ( +
+ {successMessage} + +
+ )} + + {loading ? ( +
+
+ 加载中... +
+

加载权限详情...

+
+ ) : error ? ( +
+
+ {error} +
+ {userPermissions.length > 0 && ( +
+ + + + + + + + + + + + {userPermissions.map((item) => { + const currentPermissionType = getPermissionType(item.permissions); + const updatedPermissionType = + updatedPermissions[item.knowledge_base.id] || + currentPermissionType; + + return ( + + + + + + + + ); + })} + +
知识库名称所属部门当前权限最后访问时间操作
{item.knowledge_base.name}{item.knowledge_base.department || '未指定'} + + {getPermissionTypeText(currentPermissionType)} + + + {item.granted_at + ? new Date(item.granted_at).toLocaleString() + : '从未访问'} + + +
+
+ )} +
+ ) : userPermissions.length === 0 ? ( +
+ 该用户暂无任何知识库权限 +
+ ) : ( +
+ + + + + + + + + + + + {userPermissions.map((item) => { + const currentPermissionType = getPermissionType(item.permissions); + const updatedPermissionType = + updatedPermissions[item.knowledge_base.id] || currentPermissionType; + + return ( + + + + + + + + ); + })} + +
知识库名称所属部门当前权限授权时间操作
{item.knowledge_base.name}{item.knowledge_base.department || '未指定'} + + {getPermissionTypeText(currentPermissionType)} + + + {item.granted_at + ? new Date(item.granted_at).toLocaleString() + : '未记录'} + + +
+
+ )} +
+
+ + +
+
+
+
+
+ ); +} diff --git a/src/pages/Permissions/components/UserPermissions.css b/src/pages/Permissions/components/UserPermissions.css new file mode 100644 index 0000000..1e1dd18 --- /dev/null +++ b/src/pages/Permissions/components/UserPermissions.css @@ -0,0 +1,210 @@ +.search-box { + position: relative; +} + +.search-input { + padding: 8px 12px; + border: 1px solid #d9d9d9; + border-radius: 4px; + width: 240px; + font-size: 14px; + outline: none; + transition: all 0.3s; +} + +.search-input:focus { + border-color: #1890ff; + box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2); +} + +.user-permissions-table { + background-color: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.table-header { + display: flex; + background-color: #fafafa; + border-bottom: 1px solid #f0f0f0; + padding: 12px 16px; + font-weight: 600; + color: #333; +} + +.header-cell { + flex: 1; +} + +.header-cell:first-child { + flex: 1.5; +} + +.table-row { + display: flex; + padding: 12px 16px; + border-bottom: 1px solid #f0f0f0; + transition: background-color 0.3s; +} + +.table-row:hover { + background-color: #fafafa; +} + +.table-row:last-child { + border-bottom: none; +} + +.cell { + flex: 1; + display: flex; + align-items: center; +} + +.cell:first-child { + flex: 1.5; +} + +.user-cell { + display: flex; + align-items: center; + gap: 12px; +} + +.avatar-placeholder { + width: 36px; + height: 36px; + border-radius: 50%; + background-color: #1890ff; + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: bold; + font-size: 16px; +} + +.user-info { + display: flex; + flex-direction: column; +} + +.user-name { + font-weight: 500; + color: #333; +} + +.user-username { + font-size: 12px; + color: #999; +} + +.permission-badges { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.permission-badge { + padding: 2px 8px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.permission-badge.read { + background-color: #e6f7ff; + color: #1890ff; + border: 1px solid #91d5ff; +} + +.permission-badge.edit { + background-color: #f6ffed; + color: #52c41a; + border: 1px solid #b7eb8f; +} + +.permission-badge.admin { + background-color: #fff2f0; + color: #ff4d4f; + border: 1px solid #ffccc7; +} + +.action-cell { + justify-content: flex-end; +} + +.btn-details { + background-color: transparent; + border: 1px solid #1890ff; + color: #1890ff; + padding: 4px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.btn-details:hover { + background-color: #e6f7ff; +} + +/* 分页控件样式 */ +.pagination-container { + display: flex; + justify-content: center; + align-items: center; + margin-top: 20px; + padding: 10px 0; +} + +.pagination-button { + background-color: #fff; + border: 1px solid #d9d9d9; + color: #333; + padding: 6px 12px; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + transition: all 0.2s; +} + +.pagination-button:hover:not(:disabled) { + border-color: #1890ff; + color: #1890ff; +} + +.pagination-button:disabled { + color: #d9d9d9; + cursor: not-allowed; +} + +.pagination-info { + margin: 0 15px; + color: #666; + font-size: 14px; +} + +/* 页面大小选择器样式 */ +.page-size-selector { + margin-left: 20px; + display: flex; + align-items: center; +} + +.page-size-select { + padding: 5px 10px; + border: 1px solid #d9d9d9; + border-radius: 4px; + background-color: #fff; + font-size: 14px; + color: #333; + cursor: pointer; + outline: none; + transition: all 0.3s; +} + +.page-size-select:hover, .page-size-select:focus { + border-color: #1890ff; +} \ No newline at end of file diff --git a/src/pages/Permissions/components/UserPermissions.jsx b/src/pages/Permissions/components/UserPermissions.jsx new file mode 100644 index 0000000..42dad52 --- /dev/null +++ b/src/pages/Permissions/components/UserPermissions.jsx @@ -0,0 +1,373 @@ +import React, { useState, useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { fetchAllUserPermissions, updateUserPermissions } from '../../../store/permissions/permissions.thunks'; +import UserPermissionDetails from './UserPermissionDetails'; +import './UserPermissions.css'; +import SvgIcon from '../../../components/SvgIcon'; + +// 每页显示选项 +const PAGE_SIZE_OPTIONS = [5, 10, 15, 20, 50, 100]; + +export default function UserPermissions() { + const dispatch = useDispatch(); + const [selectedUser, setSelectedUser] = useState(null); + const [showDetailsModal, setShowDetailsModal] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); + const [statusFilter, setStatusFilter] = useState('all'); // 'all', 'pending', 'approved', 'rejected' + + // 分页状态 + const [currentPage, setCurrentPage] = useState(1); + const [pageSize, setPageSize] = useState(10); + + // 从 Redux store 获取用户列表和状态 + const { + results: users, + status: loading, + error, + total, + page, + page_size, + } = useSelector((state) => state.permissions.allUsersPermissions); + + // 获取用户列表 + useEffect(() => { + dispatch(fetchAllUserPermissions({ page: currentPage, page_size: pageSize })); + }, [dispatch, currentPage, pageSize]); + + // 计算总页数 + const totalPages = Math.ceil(total / page_size); + + const handleOpenDetailsModal = (data) => { + setSelectedUser(data); + setShowDetailsModal(true); + }; + + const handleCloseDetailsModal = () => { + setSelectedUser(null); + setShowDetailsModal(false); + }; + + const handleSavePermissions = async (userId) => { + try { + // Permission updates are now handled directly in the UserPermissionDetails component + // Just refresh the users list to reflect the updated permissions + handleCloseDetailsModal(); + dispatch(fetchAllUserPermissions({ page: currentPage, page_size: pageSize })); + } catch (error) { + console.error('更新权限失败:', error); + } + }; + + const handleSearchChange = (e) => { + setSearchTerm(e.target.value); + setCurrentPage(1); // 重置到第一页 + }; + + const handleStatusFilterChange = (e) => { + setStatusFilter(e.target.value); + setCurrentPage(1); // 重置到第一页 + }; + + const handlePageChange = (page) => { + if (page > 0 && page <= totalPages) { + setCurrentPage(page); + } + }; + + const handlePageSizeChange = (e) => { + const newPageSize = parseInt(e.target.value); + setPageSize(newPageSize); + setCurrentPage(1); // 重置到第一页 + }; + + // 过滤用户列表 + const getFilteredUsers = () => { + let filtered = users; + + // 应用状态过滤 + if (statusFilter !== 'all') { + filtered = filtered.filter((user) => + user.permissions.some((permission) => permission.status === statusFilter) + ); + } + + // 应用搜索过滤 + if (searchTerm.trim()) { + filtered = filtered.filter( + (user) => + user.user_info?.username.toLowerCase().includes(searchTerm.toLowerCase()) || + user.user_info?.name.toLowerCase().includes(searchTerm.toLowerCase()) || + user.user_info?.department.toLowerCase().includes(searchTerm.toLowerCase()) || + user.user_info?.role.toLowerCase().includes(searchTerm.toLowerCase()) + ); + } + + return filtered; + }; + + const filteredUsers = getFilteredUsers(); + + const renderPagination = () => { + const pageNumbers = []; + const ellipsis = ( +
  • + ... +
  • + ); + + let startPage = Math.max(1, currentPage - 2); + let endPage = Math.min(totalPages, startPage + 4); + + if (endPage - startPage < 4) { + startPage = Math.max(1, endPage - 4); + } + + // 总是显示第一页 + if (startPage > 1) { + pageNumbers.push( +
  • + +
  • + ); + + // 如果第一页和起始页之间有间隔,显示省略号 + if (startPage > 2) { + pageNumbers.push(ellipsis); + } + } + + // 添加页码按钮 + for (let i = startPage; i <= endPage; i++) { + pageNumbers.push( +
  • + +
  • + ); + } + + // 如果末页和最后一页之间有间隔,显示省略号 + if (endPage < totalPages - 1) { + pageNumbers.push(ellipsis); + } + + // 如果末页不是最后一页,显示最后一页 + if (endPage < totalPages) { + pageNumbers.push( +
  • + +
  • + ); + } + + return ( +
    +
    + 每页显示: + + + 总计 {total} 条记录 + +
    + + +
    + ); + }; + + return ( + <> +
    +
    用户权限管理
    +
    + + <> +
    +
    +
    +
    + + + + +
    +
    + +
    +
    + + {loading === 'loading' ? ( +
    +
    + 加载中... +
    +

    加载用户权限列表...

    +
    + ) : error ? ( +
    + {error} +
    + ) : ( + <> +
    + + + + + + + + + + + + + + {filteredUsers.length > 0 ? ( + filteredUsers.map((userPermission) => ( + + + + + + + + + + )) + ) : ( + + + + )} + +
    ID用户名姓名部门角色权限类型操作
    {userPermission.user_info?.id} + {userPermission.user_info?.username || 'N/A'} + + {userPermission.user_info?.name || 'N/A'} + + {userPermission.user_info?.department || 'N/A'} + + + {userPermission.user_info?.role === 'admin' + ? '管理员' + : userPermission.user_info?.role === 'leader' + ? '组长' + : '成员'} + + +
    + {userPermission.stats?.by_permission?.full_access > 0 && ( + + 完全访问:{' '} + {userPermission.stats?.by_permission?.full_access} + + )} + + {userPermission.stats?.by_permission?.read_only > 0 && ( + + 只读访问:{' '} + {userPermission.stats?.by_permission?.read_only} + + )} + + {userPermission.stats?.by_permission?.read_write > 0 && ( + + 读写权限:{' '} + {userPermission.stats?.by_permission?.read_write} + + )} +
    +
    + +
    + 无匹配的用户记录 +
    +
    + + {totalPages > 1 && renderPagination()} + + )} + + {showDetailsModal && selectedUser && ( + + )} + + + ); +} diff --git a/src/pages/auth/Login.jsx b/src/pages/auth/Login.jsx new file mode 100644 index 0000000..60178a4 --- /dev/null +++ b/src/pages/auth/Login.jsx @@ -0,0 +1,120 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link, useNavigate } from 'react-router-dom'; +import { checkAuthThunk, loginThunk } from '../../store/auth/auth.thunk'; +import { showNotification } from '../../store/notification.slice'; + +export default function Login() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [errors, setErrors] = useState({}); + const [submitted, setSubmitted] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const { user } = useSelector((state) => state.auth); + + useEffect(() => { + handleCheckAuth(); + }, [dispatch]); + + const handleCheckAuth = async () => { + console.log('login page handleCheckAuth'); + try { + await dispatch(checkAuthThunk()).unwrap(); + if (user) navigate('/'); + } catch (error) { + // 检查登录状态失败,不需要显示通知 + } + }; + + const validateForm = () => { + const newErrors = {}; + if (!username) { + newErrors.username = '请输入用户名'; + } + if (!password) { + newErrors.password = '请输入密码'; + } else if (password.length < 6) { + newErrors.password = '密码长度不能少于6个字符'; + } + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setSubmitted(true); + + if (validateForm()) { + setIsLoading(true); + try { + await dispatch(loginThunk({ username, password })).unwrap(); + navigate('/'); + } catch (error) { + // 登录失败的错误通知已在thunk中处理 + console.error('Login failed:', error); + } finally { + setIsLoading(false); + } + } + }; + + return ( +
    +
    OOIN 达人智能知识库
    +
    +
    + setUsername(e.target.value.trim())} + > + {submitted && errors.username &&
    {errors.username}
    } +
    +
    + setPassword(e.target.value.trim())} + > + {submitted && errors.password &&
    {errors.password}
    } +
    + + 忘记密码? + + +
    + + 没有账号?去注册 + +
    + ); +} diff --git a/src/pages/auth/Signup.jsx b/src/pages/auth/Signup.jsx new file mode 100644 index 0000000..4c4498a --- /dev/null +++ b/src/pages/auth/Signup.jsx @@ -0,0 +1,274 @@ +import React, { useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { Link, useNavigate } from 'react-router-dom'; +import { checkAuthThunk, signupThunk } from '../../store/auth/auth.thunk'; + +// 部门和组别映射关系 +const departmentGroups = { + 达人部门: ['达人'], + 商务部门: ['商务'], + 样本中心: ['样本'], + 产品部门: ['产品'], + AI自媒体: ['AI自媒体'], + HR: ['HR'], + 技术部门: ['技术'], +}; + +export default function Signup() { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const [formData, setFormData] = useState({ + username: '', + email: '', + password: '', + name: '', + role: 'member', + department: '', + group: '', + }); + const [errors, setErrors] = useState({}); + const [submitted, setSubmitted] = useState(false); + const [availableGroups, setAvailableGroups] = useState([]); + + const { user, loading } = useSelector((state) => state.auth); + + useEffect(() => { + handleCheckAuth(); + }, [dispatch]); + + // 当部门变化时,更新可选的组别 + useEffect(() => { + if (formData.department && departmentGroups[formData.department]) { + setAvailableGroups(departmentGroups[formData.department]); + // 如果已选择的组别不在新部门的选项中,则重置组别 + if (!departmentGroups[formData.department].includes(formData.group)) { + setFormData(prev => ({ + ...prev, + group: '' + })); + } + } else { + setAvailableGroups([]); + setFormData(prev => ({ + ...prev, + group: '' + })); + } + }, [formData.department]); + + const handleCheckAuth = async () => { + console.log('signup page handleCheckAuth'); + try { + await dispatch(checkAuthThunk()).unwrap(); + if (user) navigate('/'); + } catch (error) {} + }; + + const handleInputChange = (e) => { + const { name, value } = e.target; + setFormData({ + ...formData, + [name]: value, + }); + + // 清除对应的错误信息 + if (errors[name]) { + setErrors({ + ...errors, + [name]: '', + }); + } + }; + + const validateForm = () => { + const newErrors = {}; + if (!formData.username) { + newErrors.username = 'Username is required'; + } + + if (!formData.email) { + newErrors.email = 'Email is required'; + } else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formData.email)) { + newErrors.email = 'Invalid email address'; + } + + if (!formData.password) { + newErrors.password = 'Password is required'; + } else if (formData.password.length < 6) { + newErrors.password = 'Password must be at least 6 characters'; + } + + if (!formData.name) { + newErrors.name = 'Name is required'; + } + + if (!formData.department) { + newErrors.department = '请选择部门'; + } + + if (!formData.group) { + newErrors.group = '请选择组别'; + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + setSubmitted(true); + + if (validateForm()) { + console.log('Form submitted successfully!'); + console.log('Registration data:', formData); + try { + await dispatch(signupThunk(formData)).unwrap(); + navigate('/login'); + } catch (error) { + console.error('Signup failed:', error); + } + } + }; + + return ( +
    +
    OOIN 达人智能知识库
    +
    +
    + + {submitted && errors.username &&
    {errors.username}
    } +
    +
    + + {submitted && errors.email &&
    {errors.email}
    } +
    +
    + + {submitted && errors.password &&
    {errors.password}
    } +
    +
    + + {submitted && errors.name &&
    {errors.name}
    } +
    +
    + + {submitted && errors.department &&
    {errors.department}
    } +
    +
    + + {submitted && errors.group &&
    {errors.group}
    } +
    +
    + +
    + +
    + + 已有账号?立即登录 + +
    + ); +} diff --git a/src/router/protectedRoute.jsx b/src/router/protectedRoute.jsx new file mode 100644 index 0000000..ac4c9d8 --- /dev/null +++ b/src/router/protectedRoute.jsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { Navigate, Outlet } from 'react-router-dom'; + +function ProtectedRoute() { + const { user } = useSelector((state) => state.auth); + + return + // return !!user ? : ; +} + +export default ProtectedRoute; diff --git a/src/router/router.jsx b/src/router/router.jsx new file mode 100644 index 0000000..6ad6601 --- /dev/null +++ b/src/router/router.jsx @@ -0,0 +1,92 @@ +import React, { Suspense } from 'react'; +import { Navigate, Route, Routes } from 'react-router-dom'; +import Mainlayout from '../layouts/Mainlayout'; +import KnowledgeBase from '../pages/KnowledgeBase/KnowledgeBase'; +import KnowledgeBaseDetail from '../pages/KnowledgeBase/Detail/KnowledgeBaseDetail'; +import Chat from '../pages/Chat/Chat'; +import PermissionsPage from '../pages/Permissions/PermissionsPage'; +import Loading from '../components/Loading'; +import Login from '../pages/Auth/Login'; +import Signup from '../pages/Auth/Signup'; +import ProtectedRoute from './protectedRoute'; +import { useSelector } from 'react-redux'; +import NotificationSnackbar from '../components/NotificationSnackbar'; + +function AppRouter() { + const { user } = useSelector((state) => state.auth); + + // 检查用户是否有管理权限(leader 或 admin) + const hasManagePermission = user && (user.role === 'leader' || user.role === 'admin'); + + return ( + }> + + + + }> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + {/* 权限管理页面路由 */} + + + + } + /> + + } /> + } /> + } /> + + + ); +} +export default AppRouter; diff --git a/src/services/api.js b/src/services/api.js new file mode 100644 index 0000000..d92e095 --- /dev/null +++ b/src/services/api.js @@ -0,0 +1,321 @@ +import axios from 'axios'; +import CryptoJS from 'crypto-js'; +import { mockGet, mockPost, mockPut, mockDelete } from './mockApi'; + +const secretKey = import.meta.env.VITE_SECRETKEY; + +// API连接状态 +let isServerDown = false; +let hasCheckedServer = false; + +// Create Axios instance with base URL +const api = axios.create({ + baseURL: '/api', + withCredentials: true, // Include cookies if needed +}); + +// Request Interceptor +api.interceptors.request.use( + (config) => { + const encryptedToken = sessionStorage.getItem('token') || ''; + if (encryptedToken) { + const decryptedToken = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8); + config.headers.Authorization = `Token ${decryptedToken}`; + } + return config; + }, + (error) => { + console.error('Request error:', error); + return Promise.reject(error); + } +); + +// Response Interceptor +api.interceptors.response.use( + (response) => { + // 如果成功收到响应,表示服务器正常工作 + if (!hasCheckedServer) { + console.log('Server is up and running'); + isServerDown = false; + hasCheckedServer = true; + } + return response; + }, + (error) => { + // 处理服务器无法连接的情况 + if (!error.response || error.code === 'ECONNABORTED' || error.message.includes('Network Error')) { + console.error('Server appears to be down. Switching to mock data.'); + isServerDown = true; + hasCheckedServer = true; + } + + // Handle errors in the response + if (error.response) { + // monitor /verify + if (error.response.status === 401 && error.config.url === '/auth/verify-token/') { + if (window.location.pathname !== '/login' && window.location.pathname !== '/signup') { + window.location.href = '/login'; + } + } + + // The request was made and the server responded with a status code + console.error('API Error Response:', error.response.status, error.response.data.message); + // alert(`Error: ${error.response.status} - ${error.response.data.message || 'Something went wrong'}`); + } else if (error.request) { + // The request was made but no response was received + console.error('API Error Request:', error.request); + // alert('Network error: No response from server'); + } else { + // Something happened in setting up the request + console.error('API Error Message:', error.message); + // alert('Error: ' + error.message); + } + return Promise.reject(error); // Reject the promise + } +); + +// 检查服务器状态 +export const checkServerStatus = async () => { + try { + // await api.get('/health-check', { timeout: 3000 }); + isServerDown = false; + hasCheckedServer = true; + console.log('Server connection established'); + return true; + } catch (error) { + isServerDown = true; + hasCheckedServer = true; + console.error('Server connection failed, using mock data'); + return false; + } +}; + +// 初始检查服务器状态 +checkServerStatus(); + +// Define common HTTP methods with fallback to mock API +const get = async (url, params = {}) => { + try { + if (isServerDown) { + console.log(`[MOCK MODE] GET ${url}`); + return await mockGet(url, params); + } + + const res = await api.get(url, { ...params }); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for GET ${url}`); + return await mockGet(url, params); + } + throw error; + } +}; + +// Handle POST requests for JSON data with fallback to mock API +const post = async (url, data, isMultipart = false) => { + try { + if (isServerDown) { + console.log(`[MOCK MODE] POST ${url}`); + return await mockPost(url, data); + } + + const headers = isMultipart + ? { 'Content-Type': 'multipart/form-data' } // For file uploads + : { 'Content-Type': 'application/json' }; // For JSON data + + const res = await api.post(url, data, { headers }); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for POST ${url}`); + return await mockPost(url, data); + } + throw error; + } +}; + +// Handle PUT requests with fallback to mock API +const put = async (url, data) => { + try { + if (isServerDown) { + console.log(`[MOCK MODE] PUT ${url}`); + return await mockPut(url, data); + } + + const res = await api.put(url, data, { + headers: { 'Content-Type': 'application/json' }, + }); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for PUT ${url}`); + return await mockPut(url, data); + } + throw error; + } +}; + +// Handle DELETE requests with fallback to mock API +const del = async (url) => { + try { + if (isServerDown) { + console.log(`[MOCK MODE] DELETE ${url}`); + return await mockDelete(url); + } + + const res = await api.delete(url); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for DELETE ${url}`); + return await mockDelete(url); + } + throw error; + } +}; + +const upload = async (url, data) => { + try { + if (isServerDown) { + console.log(`[MOCK MODE] Upload ${url}`); + return await mockPost(url, data, true); + } + + const axiosInstance = await axios.create({ + baseURL: '/api', + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + const res = await axiosInstance.post(url, data); + return res.data; + } catch (error) { + if (!hasCheckedServer || (error.request && !error.response)) { + console.log(`Failed to connect to server. Falling back to mock API for Upload ${url}`); + return await mockPost(url, data, true); + } + throw error; + } +}; + +// 手动切换到模拟API(为调试目的) +export const switchToMockApi = () => { + isServerDown = true; + hasCheckedServer = true; + console.log('Manually switched to mock API'); +}; + +// 手动切换回真实API +export const switchToRealApi = async () => { + // 重新检查服务器状态 + const isServerUp = await checkServerStatus(); + console.log(isServerUp ? 'Switched back to real API' : 'Server still down, continuing with mock API'); + return isServerUp; +}; + +// Handle streaming requests +const streamRequest = async (url, data, onChunk, onError) => { + try { + if (isServerDown) { + console.log(`[MOCK MODE] STREAM ${url}`); + // 模拟流式响应 + setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"这是模拟的","conversation_id":"mock-1234"}}'), 300); + setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"流式","conversation_id":"mock-1234"}}'), 600); + setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"响应","conversation_id":"mock-1234"}}'), 900); + setTimeout(() => onChunk('{"code":200,"message":"partial","data":{"content":"数据","conversation_id":"mock-1234","is_end":true}}'), 1200); + return { success: true, conversation_id: 'mock-1234' }; + } + + // 获取认证Token + const encryptedToken = sessionStorage.getItem('token') || ''; + let token = ''; + if (encryptedToken) { + token = CryptoJS.AES.decrypt(encryptedToken, secretKey).toString(CryptoJS.enc.Utf8); + } + + // 使用fetch API进行流式请求 + const response = await fetch(`/api${url}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Token ${token}` : '', + }, + body: JSON.stringify(data), + }); + + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + + // 获取响应体的reader + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let conversationId = null; + + // 处理流式数据 + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + // 解码并处理数据 + const chunk = decoder.decode(value, { stream: true }); + buffer += chunk; + + // 按行分割并处理JSON + const lines = buffer.split('\n'); + buffer = lines.pop() || ''; // 保留最后一行(可能不完整) + + for (const line of lines) { + if (!line.trim()) continue; + + try { + // 检查是否为SSE格式(data: {...}) + let jsonStr = line; + if (line.startsWith('data:')) { + // 提取data:后面的JSON部分 + jsonStr = line.substring(5).trim(); + console.log('检测到SSE格式数据,提取JSON:', jsonStr); + } + + // 尝试解析JSON + const data = JSON.parse(jsonStr); + if (data.code === 200 && data.data && data.data.conversation_id) { + conversationId = data.data.conversation_id; + } + onChunk(jsonStr); + } catch (e) { + console.warn('Failed to parse JSON:', line, e); + } + } + } + + return { success: true, conversation_id: conversationId }; + } catch (error) { + console.error('Streaming request failed:', error); + if (onError) { + onError(error); + } + throw error; + } +}; + +// 权限相关API +export const applyPermission = (data) => { + return post('/permissions/', data); +}; + +export const updatePermission = (data) => { + return post('/permissions/update_permission/', data); +}; + +export const approvePermission = (permissionId) => { + return post(`/permissions/approve_permission/${permissionId}`); +}; + +export const rejectPermission = (permissionId) => { + return post(`/permissions/reject_permission/${permissionId}`); +}; + +export { get, post, put, del, upload, streamRequest }; diff --git a/src/services/mockApi.js b/src/services/mockApi.js new file mode 100644 index 0000000..dd31eb5 --- /dev/null +++ b/src/services/mockApi.js @@ -0,0 +1,1131 @@ +// Mock API service for development without backend +import { v4 as uuidv4 } from 'uuid'; + +// Mock data for knowledge bases +const mockKnowledgeBases = [ + { + id: uuidv4(), + user_id: 'user-001', + name: 'Frontend Development', + desc: 'Resources and guides for frontend development including React, Vue, and Angular', + type: 'private', + department: '研发部', + group: '前端开发组', + documents: [], + char_length: 0, + document_count: 0, + external_id: uuidv4(), + create_time: '2024-02-26T08:30:00Z', + update_time: '2024-02-26T14:45:00Z', + permissions: { + can_read: true, + can_edit: true, + can_delete: false, + }, + }, + { + id: uuidv4(), + user_id: 'user-001', + name: 'Backend Technologies', + desc: 'Information about backend frameworks, databases, and server configurations', + type: 'private', + department: '研发部', + group: '后端开发组', + documents: [], + char_length: 0, + document_count: 0, + external_id: uuidv4(), + create_time: '2024-02-25T10:15:00Z', + update_time: '2024-02-26T09:20:00Z', + permissions: { + can_read: true, + can_edit: true, + can_delete: false, + }, + }, + { + id: 'kb-003', + name: 'DevOps Practices', + description: 'Best practices for CI/CD, containerization, and cloud deployment', + created_at: '2023-11-12T15:45:00Z', + updated_at: '2024-02-05T11:30:00Z', + create_time: '2023-11-12T15:45:00Z', + update_time: '2024-02-05T11:30:00Z', + type: 'public', + owner: { + id: 'user-002', + username: 'janedoe', + email: 'jane@example.com', + }, + document_count: 18, + tags: ['docker', 'kubernetes', 'aws'], + permissions: { + can_edit: false, + can_read: true, + }, + }, + { + id: 'kb-004', + name: 'Machine Learning Fundamentals', + description: 'Introduction to machine learning concepts, algorithms, and frameworks', + created_at: '2023-08-20T09:00:00Z', + updated_at: '2024-01-25T16:15:00Z', + create_time: '2023-08-20T09:00:00Z', + update_time: '2024-01-25T16:15:00Z', + type: 'public', + owner: { + id: 'user-003', + username: 'alexsmith', + email: 'alex@example.com', + }, + document_count: 30, + tags: ['ml', 'python', 'tensorflow'], + permissions: { + can_edit: false, + can_read: true, + }, + }, + { + id: 'kb-005', + name: 'UI/UX Design Principles', + description: 'Guidelines for creating effective and user-friendly interfaces', + created_at: '2023-12-01T13:20:00Z', + updated_at: '2024-02-15T10:45:00Z', + create_time: '2023-12-01T13:20:00Z', + update_time: '2024-02-15T10:45:00Z', + type: 'private', + owner: { + id: 'user-002', + username: 'janedoe', + email: 'jane@example.com', + }, + document_count: 12, + tags: ['design', 'ui', 'ux'], + permissions: { + can_edit: true, + can_read: true, + }, + }, + { + id: 'kb-006', + name: 'Mobile App Development', + description: 'Resources for iOS, Android, and cross-platform mobile development', + created_at: '2023-10-25T11:10:00Z', + updated_at: '2024-01-30T14:00:00Z', + create_time: '2023-10-25T11:10:00Z', + update_time: '2024-01-30T14:00:00Z', + type: 'private', + owner: { + id: 'user-001', + username: 'johndoe', + email: 'john@example.com', + }, + document_count: 20, + tags: ['mobile', 'react-native', 'flutter'], + permissions: { + can_edit: true, + can_read: true, + }, + }, + { + id: 'kb-007', + name: 'Cybersecurity Best Practices', + description: 'Guidelines for securing applications, networks, and data', + created_at: '2023-09-18T14:30:00Z', + updated_at: '2024-02-10T09:15:00Z', + create_time: '2023-09-18T14:30:00Z', + update_time: '2024-02-10T09:15:00Z', + type: 'private', + owner: { + id: 'user-004', + username: 'sarahwilson', + email: 'sarah@example.com', + }, + document_count: 25, + tags: ['security', 'encryption', 'authentication'], + permissions: { + can_edit: false, + can_read: false, + }, + }, + { + id: 'kb-008', + name: 'Data Science Toolkit', + description: 'Tools and techniques for data analysis, visualization, and modeling', + created_at: '2023-11-05T10:00:00Z', + updated_at: '2024-01-20T15:30:00Z', + create_time: '2023-11-05T10:00:00Z', + update_time: '2024-01-20T15:30:00Z', + type: 'public', + owner: { + id: 'user-003', + username: 'alexsmith', + email: 'alex@example.com', + }, + document_count: 28, + tags: ['data-science', 'python', 'visualization'], + permissions: { + can_edit: false, + can_read: true, + }, + }, +]; + +// In-memory store for CRUD operations +let knowledgeBases = [...mockKnowledgeBases]; + +// Mock user data for authentication +const mockUsers = [ + { + id: 'user-001', + username: 'leader2', + password: 'leader123', // 在实际应用中不应该存储明文密码 + email: 'admin@example.com', + name: '管理员', + department: '研发部', + group: '前端开发组', + role: 'admin', + avatar: null, + created_at: '2024-01-01T00:00:00Z', + updated_at: '2024-01-01T00:00:00Z', + }, + { + id: 'user-002', + username: 'user', + password: 'user123', // 在实际应用中不应该存储明文密码 + email: 'user@example.com', + name: '普通用户', + department: '市场部', + group: '市场组', + role: 'user', + avatar: null, + created_at: '2024-01-02T00:00:00Z', + updated_at: '2024-01-02T00:00:00Z', + }, +]; + +// Helper function for pagination +const paginate = (array, page_size, page) => { + const startIndex = (page - 1) * page_size; + const endIndex = startIndex + page_size; + const items = array.slice(startIndex, endIndex); + + return { + items, + total: array.length, + page, + page_size, + }; +}; + +// Mock chat history data +const mockChatHistory = [ + { + id: 'chat-001', + title: '关于React组件开发的问题', + knowledge_base_id: 'kb-001', + knowledge_base_name: 'Frontend Development', + message_count: 5, + created_at: '2024-03-15T10:30:00Z', + updated_at: '2024-03-15T11:45:00Z', + }, + { + id: 'chat-002', + title: 'Vue.js性能优化讨论', + knowledge_base_id: 'kb-001', + knowledge_base_name: 'Frontend Development', + message_count: 3, + created_at: '2024-03-14T15:20:00Z', + updated_at: '2024-03-14T16:10:00Z', + }, + { + id: 'chat-003', + title: '后端API集成问题', + knowledge_base_id: 'kb-002', + knowledge_base_name: 'Backend Technologies', + message_count: 4, + created_at: '2024-03-13T09:15:00Z', + updated_at: '2024-03-13T10:30:00Z', + }, +]; + +// Mock chat history functions +const mockGetChatHistory = (params) => { + const { page = 1, page_size = 10 } = params; + return paginate(mockChatHistory, page_size, page); +}; + +const mockCreateChat = (data) => { + const newChat = { + id: `chat-${uuidv4().slice(0, 8)}`, + title: data.title || '新对话', + knowledge_base_id: data.knowledge_base_id, + knowledge_base_name: data.knowledge_base_name, + message_count: 0, + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + }; + mockChatHistory.unshift(newChat); + return newChat; +}; + +const mockUpdateChat = (id, data) => { + const index = mockChatHistory.findIndex((chat) => chat.id === id); + if (index === -1) { + throw new Error('Chat not found'); + } + const updatedChat = { + ...mockChatHistory[index], + ...data, + updated_at: new Date().toISOString(), + }; + mockChatHistory[index] = updatedChat; + return updatedChat; +}; + +const mockDeleteChat = (id) => { + const index = mockChatHistory.findIndex((chat) => chat.id === id); + if (index === -1) { + throw new Error('Chat not found'); + } + mockChatHistory.splice(index, 1); + return { success: true }; +}; + +// 模拟聊天消息数据 +const chatMessages = {}; + +// 权限申请列表的 mock 数据 +const mockPendingRequests = [ + { + id: 1, + knowledge_base: 'f13c4bdb-eb03-4ce2-b83c-30917351fb72', + applicant: 'f2799611-7a3d-436d-b3fa-3789bdd877e2', + permissions: { + can_edit: false, + can_read: true, + can_delete: false, + }, + status: 'pending', + reason: '需要访问知识库进行学习', + response_message: null, + expires_at: '2025-03-19T00:17:43.781000Z', + created_at: '2025-03-12T00:17:44.044351Z', + updated_at: '2025-03-12T00:17:44.044369Z', + }, + { + id: 2, + knowledge_base: 'f13c4bdb-eb03-4ce2-b83c-30917351fb73', + applicant: 'f2799611-7a3d-436d-b3fa-3789bdd877e3', + permissions: { + can_edit: true, + can_read: true, + can_delete: false, + }, + status: 'pending', + reason: '需要编辑和更新文档', + response_message: null, + expires_at: '2025-03-20T00:17:43.781000Z', + created_at: '2025-03-12T00:17:44.044351Z', + updated_at: '2025-03-12T00:17:44.044369Z', + }, +]; + +// 用户权限列表的 mock 数据 +const mockUserPermissions = [ + { + id: 'perm-001', + user: { + id: 'user-001', + username: 'johndoe', + name: 'John Doe', + email: 'john@example.com', + department: '研发部', + group: '前端开发组', + }, + knowledge_base: { + id: 'kb-001', + name: 'Frontend Development Guide', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: true, + can_manage: true, + }, + granted_at: '2024-01-15T10:00:00Z', + granted_by: { + id: 'user-admin', + username: 'admin', + name: 'System Admin', + }, + }, + { + id: 'perm-002', + user: { + id: 'user-002', + username: 'janedoe', + name: 'Jane Doe', + email: 'jane@example.com', + department: '研发部', + group: '前端开发组', + }, + knowledge_base: { + id: 'kb-001', + name: 'Frontend Development Guide', + }, + permissions: { + can_read: true, + can_edit: true, + can_delete: false, + can_manage: false, + }, + granted_at: '2024-01-20T14:30:00Z', + granted_by: { + id: 'user-001', + username: 'johndoe', + name: 'John Doe', + }, + }, + { + id: 'perm-003', + user: { + id: 'user-003', + username: 'alexsmith', + name: 'Alex Smith', + email: 'alex@example.com', + department: '研发部', + group: '后端开发组', + }, + knowledge_base: { + id: 'kb-001', + name: 'Frontend Development Guide', + }, + permissions: { + can_read: true, + can_edit: false, + can_delete: false, + can_manage: false, + }, + granted_at: '2024-02-01T09:15:00Z', + granted_by: { + id: 'user-001', + username: 'johndoe', + name: 'John Doe', + }, + }, +]; + +// Mock API handlers for permissions +const mockPermissionApi = { + // 获取待处理的权限申请列表 + getPendingRequests: () => { + return { + code: 200, + message: 'success', + data: { + items: mockPendingRequests, + total: mockPendingRequests.length, + }, + }; + }, + + // 获取用户权限列表 + getUserPermissions: (knowledgeBaseId) => { + const permissions = mockUserPermissions.filter((perm) => perm.knowledge_base.id === knowledgeBaseId); + return { + code: 200, + message: 'success', + data: { + items: permissions, + total: permissions.length, + }, + }; + }, + + // 处理权限申请 + handlePermissionRequest: (requestId, action) => { + const request = mockPendingRequests.find((req) => req.id === requestId); + if (!request) { + return { + code: 404, + message: 'Permission request not found', + }; + } + + request.status = action === 'approve' ? 'approved' : 'rejected'; + + if (action === 'approve') { + // 如果批准,添加新的权限记录 + const newPermission = { + id: `perm-${Date.now()}`, + user: request.user, + knowledge_base: request.knowledge_base, + permissions: { + can_read: true, + can_edit: request.request_type === 'edit', + can_delete: false, + can_manage: false, + }, + granted_at: new Date().toISOString(), + granted_by: mockCurrentUser, + }; + mockUserPermissions.push(newPermission); + } + + return { + code: 200, + message: 'success', + data: request, + }; + }, + + // 更新用户权限 + updateUserPermission: (permissionId, permissions) => { + const permission = mockUserPermissions.find((perm) => perm.id === permissionId); + if (!permission) { + return { + code: 404, + message: 'Permission not found', + }; + } + + permission.permissions = { + ...permission.permissions, + ...permissions, + }; + + return { + code: 200, + message: 'success', + data: permission, + }; + }, + + // 删除用户权限 + deleteUserPermission: (permissionId) => { + const index = mockUserPermissions.findIndex((perm) => perm.id === permissionId); + if (index === -1) { + return { + code: 404, + message: 'Permission not found', + }; + } + + mockUserPermissions.splice(index, 1); + + return { + code: 200, + message: 'success', + }; + }, +}; + +// Mock API functions +export const mockGet = async (url, params = {}) => { + console.log(`[MOCK API] GET ${url}`, params); + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 500)); + + // Get current user + if (url === '/users/me/') { + return { + user: mockUsers[0], // 默认返回第一个用户 + }; + } + + // Get knowledge bases + if (url === '/knowledge-bases/') { + const result = paginate(knowledgeBases, params.page_size, params.page); + + return { + data: { + code: 200, + message: '获取知识库列表成功', + data: { + total: result.total, + page: result.page, + page_size: result.page_size, + items: result.items, + }, + }, + }; + } + + // Get knowledge base details + if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) { + const id = url.split('/')[2]; + const knowledgeBase = knowledgeBases.find((kb) => kb.id === id); + + if (!knowledgeBase) { + throw { response: { status: 404, data: { message: 'Knowledge base not found' } } }; + } + + return { + data: { + code: 200, + message: 'success', + data: { + knowledge_base: knowledgeBase, + }, + }, + }; + } + + // Get chat history + if (url === '/chat-history/') { + const result = mockGetChatHistory(params); + return { + data: { + code: 200, + message: 'success', + data: result, + }, + }; + } + + // Get chat messages + if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) { + const chatId = url.split('/')[2]; + + // 如果没有该聊天的消息记录,创建一个空数组 + if (!chatMessages[chatId]) { + chatMessages[chatId] = []; + + // 添加一条欢迎消息 + const chat = mockChatHistory.find((chat) => chat.id === chatId); + if (chat) { + chatMessages[chatId].push({ + id: uuidv4(), + chat_id: chatId, + sender: 'bot', + content: `欢迎使用 ${chat.knowledge_base_name},有什么可以帮助您的?`, + type: 'text', + created_at: new Date().toISOString(), + }); + } + } + + return { + code: 200, + message: '获取成功', + data: { + messages: chatMessages[chatId] || [], + }, + }; + } + + // Knowledge base search + if (url === '/knowledge-bases/search/') { + const { keyword = '', page = 1, page_size = 10 } = params.params || {}; + const filtered = knowledgeBases.filter( + (kb) => + kb.name.toLowerCase().includes(keyword.toLowerCase()) || + kb.description.toLowerCase().includes(keyword.toLowerCase()) || + kb.tags.some((tag) => tag.toLowerCase().includes(keyword.toLowerCase())) + ); + const result = paginate(filtered, page_size, page); + return { + code: 200, + message: 'success', + data: result, + }; + } + + // 用户权限管理 - 获取用户列表 + if (url === '/users/permissions/') { + return { + code: 200, + message: 'success', + data: { + users: mockUsers, + }, + }; + } + + // 用户权限管理 - 获取待处理申请 + if (url === '/permissions/pending/') { + return { + code: 200, + message: 'success', + data: { + items: mockPendingRequests, + total: mockPendingRequests.length, + }, + }; + } + + // 用户权限管理 - 获取用户权限详情 + if (url.match(/\/users\/(.+)\/permissions\//)) { + const userId = url.match(/\/users\/(.+)\/permissions\//)[1]; + + return { + code: 200, + message: 'success', + data: { + permissions: mockUserPermissions[userId] || [], + }, + }; + } + + // 获取知识库文档列表 + if (url.match(/\/knowledge-bases\/([^/]+)\/documents\//)) { + const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/documents\//)[1]; + const page = params?.params?.page || 1; + const page_size = params?.params?.page_size || 10; + + // 模拟文档列表数据 + const mockDocuments = [ + { + id: 'df6d2c2b-895c-4c56-83c8-1644345e654d', + document_id: '772044ae-0ecf-11f0-8082-0242ac120002', + document_name: '产品说明书.pdf', + external_id: '772044ae-0ecf-11f0-8082-0242ac120002', + create_time: '2023-04-01 08:01:06', + update_time: '2023-04-01 08:01:06', + }, + { + id: 'eba8f519-debf-461c-b4fd-87177d94bece', + document_id: '429a2c08-0ea3-11f0-bdec-0242ac120002', + document_name: '用户手册.docx', + external_id: '429a2c08-0ea3-11f0-bdec-0242ac120002', + create_time: '2023-04-01 02:44:38', + update_time: '2023-04-01 02:44:38', + }, + { + id: '7a9e4c31-5b2d-437e-9a8f-2b5c7e8a9d1e', + document_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e', + document_name: '技术文档.txt', + external_id: 'c9a8f2b5-7e8a-9d1e-7a9e-4c315b2d437e', + create_time: '2023-03-15 10:23:45', + update_time: '2023-03-15 10:23:45', + }, + ]; + + return { + data: { + code: 200, + message: '获取文档列表成功', + data: { + total: mockDocuments.length, + page: page, + page_size: page_size, + items: mockDocuments, + }, + }, + }; + } + + throw { response: { status: 404, data: { message: 'Not found' } } }; +}; + +export const mockPost = async (url, data, isMultipart = false) => { + console.log(`[MOCK API] POST ${url}`, data); + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 800)); + + // Login + if (url === '/auth/login/') { + const { username, password } = data; + const user = mockUsers.find((u) => u.username === username && u.password === password); + + if (!user) { + throw { + response: { + status: 401, + data: { + code: 401, + message: '用户名或密码错误', + }, + }, + }; + } + + // 在实际应用中,这里应该生成 JWT token + const token = `mock-jwt-token-${uuidv4()}`; + + return { + code: 200, + message: '登录成功', + data: { + token, + id: user.id, + username: user.username, + email: user.email, + name: user.name, + department: user.department, + group: user.group, + role: user.role, + avatar: user.avatar, + }, + }; + } + + // Create knowledge base + if (url === '/knowledge-bases/') { + const newKnowledgeBase = { + id: `kb-${uuidv4().slice(0, 8)}`, + name: data.name, + description: data.description || '', + desc: data.desc || data.description || '', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + create_time: new Date().toISOString(), + update_time: new Date().toISOString(), + type: data.type || 'private', + department: data.department || null, + group: data.group || null, + owner: { + id: 'user-001', + username: 'johndoe', + email: 'john@example.com', + }, + document_count: 0, + tags: data.tags || [], + permissions: { + can_edit: true, + can_read: true, + }, + documents: [], + }; + + knowledgeBases.push(newKnowledgeBase); + + return { + code: 200, + message: '知识库创建成功', + data: { + knowledge_base: newKnowledgeBase, + external_id: uuidv4(), + }, + }; + } + + // Create new chat + if (url === '/chat-history/') { + const newChat = mockCreateChat(data); + return { + code: 200, + message: 'success', + data: { + chat: newChat, + }, + }; + } + + // Send chat message + if (url.match(/^\/chat-history\/[^/]+\/messages\/$/)) { + const chatId = url.split('/')[2]; + + // 如果没有该聊天的消息记录,创建一个空数组 + if (!chatMessages[chatId]) { + chatMessages[chatId] = []; + } + + // 创建用户消息 + const userMessage = { + id: uuidv4(), + chat_id: chatId, + sender: 'user', + content: data.content, + type: data.type || 'text', + created_at: new Date().toISOString(), + }; + + // 添加用户消息 + chatMessages[chatId].push(userMessage); + + // 创建机器人回复 + const botMessage = { + id: uuidv4(), + chat_id: chatId, + sender: 'bot', + content: `这是对您问题的回复:${data.content}`, + type: 'text', + created_at: new Date(Date.now() + 1000).toISOString(), + }; + + // 添加机器人回复 + chatMessages[chatId].push(botMessage); + + // 更新聊天的最后一条消息和时间 + const chatIndex = mockChatHistory.findIndex((chat) => chat.id === chatId); + if (chatIndex !== -1) { + mockChatHistory[chatIndex].message_count = (mockChatHistory[chatIndex].message_count || 0) + 2; + mockChatHistory[chatIndex].updated_at = new Date().toISOString(); + } + + return { + code: 200, + message: '发送成功', + data: { + user_message: userMessage, + bot_message: botMessage, + }, + }; + } + + // 批准权限申请 + if (url === '/permissions/approve/') { + const { id, responseMessage } = data; + + // 从待处理列表中移除该申请 + mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id); + + return { + code: 200, + message: 'Permission approved successfully', + data: { + id: id, + status: 'approved', + response_message: responseMessage, + }, + }; + } + + // 创建会话 (不发送消息) + if (url === '/chat-history/create_conversation') { + const { dataset_id_list } = data; + + // 生成新的会话ID + const conversation_id = `conv-${uuidv4()}`; + + console.log(`[MOCK API] 创建新会话: ${conversation_id}, 知识库: ${dataset_id_list.join(', ')}`); + + return { + code: 200, + message: '会话创建成功', + data: { + conversation_id: conversation_id, + dataset_id_list: dataset_id_list, + }, + }; + } + + // 拒绝权限申请 + if (url === '/permissions/reject/') { + const { id, responseMessage } = data; + + // 从待处理列表中移除该申请 + mockPendingRequests = mockPendingRequests.filter((request) => request.id !== id); + + return { + code: 200, + message: 'Permission rejected successfully', + data: { + id: id, + status: 'rejected', + response_message: responseMessage, + }, + }; + } + + // 上传知识库文档 + if (url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)) { + const knowledge_base_id = url.match(/\/knowledge-bases\/([^/]+)\/upload_document\//)[1]; + const file = isMultipart ? data.get('file') : null; + + return { + data: { + code: 200, + message: 'Document uploaded successfully', + data: { + id: `doc-${Date.now()}`, + knowledge_base_id: knowledge_base_id, + filename: file ? file.name : 'mock-document.pdf', + status: 'processing', + created_at: new Date().toISOString(), + }, + }, + }; + } + + throw { response: { status: 404, data: { message: 'Not found' } } }; +}; + +export const mockPut = async (url, data) => { + console.log(`[MOCK API] PUT ${url}`, data); + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 600)); + + // Update knowledge base + if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) { + const id = url.split('/')[2]; + const index = knowledgeBases.findIndex((kb) => kb.id === id); + + if (index === -1) { + throw { response: { status: 404, data: { message: 'Knowledge base not found' } } }; + } + + const updatedKnowledgeBase = { + ...knowledgeBases[index], + ...data, + updated_at: new Date().toISOString(), + update_time: new Date().toISOString(), + }; + + knowledgeBases[index] = updatedKnowledgeBase; + + // 返回与 mockPost 类似的格式 + return { + code: 200, + message: '知识库更新成功', + data: { + knowledge_base: updatedKnowledgeBase, + external_id: knowledgeBases[index].id, + }, + }; + } + + // Update chat + if (url.match(/^\/chat-history\/[^/]+\/$/)) { + const id = url.split('/')[2]; + return { data: mockUpdateChat(id, data) }; + } + + // 更新用户权限 + if (url.match(/\/users\/(.+)\/permissions\//)) { + const userId = url.match(/\/users\/(.+)\/permissions\//)[1]; + const { permissions } = data; + + // 将权限更新应用到模拟数据 + if (mockUserPermissions[userId]) { + // 遍历permissions对象,更新对应知识库的权限 + Object.entries(permissions).forEach(([knowledgeBaseId, permissionType]) => { + // 查找该用户的该知识库权限 + const permissionIndex = mockUserPermissions[userId].findIndex( + (p) => p.knowledge_base.id === knowledgeBaseId + ); + + if (permissionIndex !== -1) { + // 根据权限类型设置具体权限 + const permission = { + can_read: permissionType === 'read' || permissionType === 'edit' || permissionType === 'admin', + can_edit: permissionType === 'edit' || permissionType === 'admin', + can_admin: permissionType === 'admin', + }; + + // 更新权限 + mockUserPermissions[userId][permissionIndex].permission = permission; + } + }); + } + + return { + code: 200, + message: 'Permissions updated successfully', + data: { + permissions: permissions, + }, + }; + } + + throw { response: { status: 404, data: { message: 'Not found' } } }; +}; + +export const mockDelete = async (url) => { + console.log(`[MOCK API] DELETE ${url}`); + + // Simulate network delay + await new Promise((resolve) => setTimeout(resolve, 800)); + + // Delete knowledge base + if (url.match(/^\/knowledge-bases\/[^/]+\/$/)) { + const id = url.split('/')[2]; + const index = knowledgeBases.findIndex((kb) => kb.id === id); + + if (index === -1) { + throw { response: { status: 404, data: { message: 'Knowledge base not found' } } }; + } + + knowledgeBases.splice(index, 1); + return { success: true }; + } + + // Delete chat (new endpoint) + if (url.match(/^\/chat-history\/delete_conversation/)) { + const params = new URLSearchParams(url.split('?')[1]); + const conversationId = params.get('conversation_id'); + + if (!conversationId) { + throw { response: { status: 400, data: { message: 'Missing conversation_id parameter' } } }; + } + + console.log(`[MOCK API] Deleting conversation: ${conversationId}`); + + // 查找并删除会话 + const index = mockChatHistory.findIndex( + (chat) => chat.id === conversationId || chat.conversation_id === conversationId + ); + + if (index === -1) { + // 即使找不到也返回成功,保持与API一致的行为 + console.log(`[MOCK API] Conversation not found: ${conversationId}, but returning success`); + return { + code: 200, + message: '会话删除成功', + data: {}, + }; + } + + mockChatHistory.splice(index, 1); + + // 清除会话消息 + if (chatMessages[conversationId]) { + delete chatMessages[conversationId]; + } + + return { + code: 200, + message: '会话删除成功', + data: {}, + }; + } + + // Delete chat (old endpoint - keeping for backward compatibility) + if (url.match(/^\/chat-history\/[^/]+\/$/)) { + const id = url.split('/')[2]; + return { data: mockDeleteChat(id) }; + } + + // 删除知识库文档 + if (url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/)) { + const matches = url.match(/\/knowledge-bases\/([^/]+)\/documents\/([^/]+)/); + const knowledge_base_id = matches[1]; + const document_id = matches[2]; + + console.log(`[MOCK API] Deleting document ${document_id} from knowledge base ${knowledge_base_id}`); + + return { + data: { + code: 200, + message: '文档删除成功', + }, + }; + } + + throw { response: { status: 404, data: { message: 'Not found' } } }; +}; + +// Reset mock data to initial state (useful for testing) +export const resetMockData = () => { + knowledgeBases = [...mockKnowledgeBases]; +}; + +// 添加权限相关的 API 处理 +export const mockApi = { + // ... existing api handlers ... + + // 权限管理相关的 API + 'GET /api/permissions/pending': () => mockPermissionApi.getPendingRequests(), + 'GET /api/permissions/users/:knowledgeBaseId': (params) => + mockPermissionApi.getUserPermissions(params.knowledgeBaseId), + 'POST /api/permissions/handle/:requestId': (params, body) => + mockPermissionApi.handlePermissionRequest(params.requestId, body.action), + 'PUT /api/permissions/:permissionId': (params, body) => + mockPermissionApi.updateUserPermission(params.permissionId, body.permissions), + 'DELETE /api/permissions/:permissionId': (params) => mockPermissionApi.deleteUserPermission(params.permissionId), +}; diff --git a/src/services/permissionService.js b/src/services/permissionService.js new file mode 100644 index 0000000..a77532e --- /dev/null +++ b/src/services/permissionService.js @@ -0,0 +1,56 @@ +import { post } from './api'; + +/** + * 计算权限过期时间 + * @param {string} duration - 权限持续时间,如 '一周', '一个月', '三个月', '六个月', '永久' + * @returns {string} - ISO 格式的日期字符串 + */ +export const calculateExpiresAt = (duration) => { + const now = new Date(); + switch (duration) { + case '一周': + now.setDate(now.getDate() + 7); + break; + case '一个月': + now.setMonth(now.getMonth() + 1); + break; + case '三个月': + now.setMonth(now.getMonth() + 3); + break; + case '六个月': + now.setMonth(now.getMonth() + 6); + break; + case '永久': + // 设置为较远的未来日期 + now.setFullYear(now.getFullYear() + 10); + break; + default: + now.setDate(now.getDate() + 7); + } + return now.toISOString(); +}; + +/** + * 申请知识库访问权限(已废弃,请使用store/knowledgeBase/knowledgeBase.thunks中的requestKnowledgeBaseAccess) + * @deprecated 请使用Redux thunk版本 + * @param {Object} requestData - 请求数据 + * @param {string} requestData.id - 知识库ID + * @param {string} requestData.accessType - 访问类型,如 '只读访问', '编辑权限' + * @param {string} requestData.duration - 访问时长,如 '一周', '一个月' + * @param {string} requestData.reason - 申请原因 + * @returns {Promise} - API 请求的 Promise + */ +export const legacyRequestKnowledgeBaseAccess = async (requestData) => { + const apiRequestData = { + knowledge_base: requestData.id, + permissions: { + can_read: true, + can_edit: requestData.accessType === '编辑权限', + can_delete: false, + }, + reason: requestData.reason, + expires_at: calculateExpiresAt(requestData.duration), + }; + + return post('/permissions/', apiRequestData); +}; diff --git a/src/services/userServices.js b/src/services/userServices.js new file mode 100644 index 0000000..6facc91 --- /dev/null +++ b/src/services/userServices.js @@ -0,0 +1,13 @@ +import { get, post } from "./api"; + +export const checkToken = async () => { + const response = await get("/check-token"); + const { user, message } = response; + return user; +}; + +export const login = async (config) => { + const response = await post('login', config); + const {message, user} = response; + return user; +} \ No newline at end of file diff --git a/src/services/websocket.js b/src/services/websocket.js new file mode 100644 index 0000000..9902e19 --- /dev/null +++ b/src/services/websocket.js @@ -0,0 +1,245 @@ +import { addNotification, markNotificationAsRead } from '../store/notificationCenter/notificationCenter.slice'; +import store from '../store/store'; // 修改为默认导出 + +// 从环境变量获取 API URL +const API_URL = import.meta.env.VITE_API_URL || 'http://81.69.223.133:8008'; +// 将 HTTP URL 转换为 WebSocket URL +const WS_BASE_URL = API_URL.replace(/^http/, 'ws').replace(/\/api\/?$/, ''); + +let socket = null; +let reconnectTimer = null; +let pingInterval = null; +let reconnectAttempts = 0; // 添加重连尝试计数器 +const RECONNECT_DELAY = 5000; // 5秒后尝试重连 +const PING_INTERVAL = 30000; // 30秒发送一次ping +const MAX_RECONNECT_ATTEMPTS = 3; // 最大重连尝试次数 + +/** + * 初始化WebSocket连接 + * @returns {Promise} WebSocket连接实例 + */ +export const initWebSocket = () => { + return new Promise((resolve, reject) => { + // 如果已经有一个连接,先关闭它 + if (socket && socket.readyState !== WebSocket.CLOSED) { + socket.close(); + } + + // 清除之前的定时器 + if (reconnectTimer) clearTimeout(reconnectTimer); + if (pingInterval) clearInterval(pingInterval); + + try { + // 从sessionStorage获取token + const encryptedToken = sessionStorage.getItem('token'); + if (!encryptedToken) { + console.error('No token found, cannot connect to notification service'); + reject(new Error('No token found')); + return; + } + + const wsUrl = `${WS_BASE_URL}/ws/notifications?token=${encryptedToken}`; + console.log('WebSocket URL:', wsUrl); + socket = new WebSocket(wsUrl); + + // 连接建立时的处理 + socket.onopen = () => { + console.log('WebSocket connection established'); + reconnectAttempts = 0; // 连接成功后重置重连计数器 + + // 订阅通知频道 + subscribeToNotifications(); + + // 设置定时发送ping消息 + pingInterval = setInterval(() => { + if (socket.readyState === WebSocket.OPEN) { + sendPing(); + } + }, PING_INTERVAL); + + resolve(socket); + }; + + // 接收消息的处理 + socket.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + handleWebSocketMessage(data); + } catch (error) { + console.error('Error parsing WebSocket message:', error); + } + }; + + // 错误处理 + socket.onerror = (error) => { + console.error('WebSocket error:', error); + reject(error); + }; + + // 连接关闭时的处理 + socket.onclose = (event) => { + console.log(`WebSocket connection closed: ${event.code} ${event.reason}`); + + // 清除ping定时器 + if (pingInterval) clearInterval(pingInterval); + + // 如果不是正常关闭,尝试重连 + if (event.code !== 1000) { + if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) { + reconnectAttempts++; + console.log(`WebSocket reconnection attempt ${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS}`); + + reconnectTimer = setTimeout(() => { + console.log('Attempting to reconnect WebSocket...'); + initWebSocket().catch((err) => { + console.error('Failed to reconnect WebSocket:', err); + }); + }, RECONNECT_DELAY); + } else { + console.log('Maximum reconnection attempts reached. Giving up.'); + } + } + }; + } catch (error) { + console.error('Error initializing WebSocket:', error); + reject(error); + } + }); +}; + +/** + * 订阅通知频道 + */ +export const subscribeToNotifications = () => { + if (socket && socket.readyState === WebSocket.OPEN) { + const subscribeMessage = { + type: 'subscribe', + channel: 'notifications', + }; + socket.send(JSON.stringify(subscribeMessage)); + } +}; + +/** + * 发送ping消息(保持连接活跃) + */ +export const sendPing = () => { + if (socket && socket.readyState === WebSocket.OPEN) { + const pingMessage = { + type: 'ping', + }; + socket.send(JSON.stringify(pingMessage)); + } +}; + +/** + * 确认已读通知 + * @param {string} notificationId 通知ID + */ +export const acknowledgeNotification = (notificationId) => { + if (socket && socket.readyState === WebSocket.OPEN) { + const ackMessage = { + type: 'acknowledge', + notification_id: notificationId, + }; + socket.send(JSON.stringify(ackMessage)); + + // 使用 store.dispatch 替代 dispatch + store.dispatch(markNotificationAsRead(notificationId)); + } +}; + +/** + * 关闭WebSocket连接 + */ +export const closeWebSocket = () => { + if (socket) { + socket.close(1000, 'Normal closure'); + socket = null; + } + + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + + if (pingInterval) { + clearInterval(pingInterval); + pingInterval = null; + } +}; + +/** + * 处理接收到的WebSocket消息 + * @param {Object} data 解析后的消息数据 + */ +const handleWebSocketMessage = (data) => { + switch (data.type) { + case 'connection_established': + console.log(`Connection established for user: ${data.user_id}`); + break; + + case 'notification': + console.log('Received notification:', data); + // 将通知添加到Redux store + store.dispatch(addNotification(processNotification(data))); + break; + + case 'pong': + console.log(`Received pong at ${data.timestamp}`); + break; + + case 'error': + console.error(`WebSocket error: ${data.code} - ${data.message}`); + break; + + default: + console.log('Received unknown message type:', data); + } +}; + +/** + * 处理通知数据,转换为应用内通知格式 + * @param {Object} data 通知数据 + * @returns {Object} 处理后的通知数据 + */ +const processNotification = (data) => { + const { data: notificationData } = data; + + let icon = 'bi-info-circle'; + if (notificationData.category === 'system') { + icon = 'bi-info-circle'; + } else if (notificationData.category === 'permission') { + icon = 'bi-shield'; + } + + // 计算时间显示 + const createdAt = new Date(notificationData.created_at); + const now = new Date(); + const diffMs = now - createdAt; + const diffMins = Math.floor(diffMs / 60000); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + let timeDisplay; + if (diffMins < 60) { + timeDisplay = `${diffMins}分钟前`; + } else if (diffHours < 24) { + timeDisplay = `${diffHours}小时前`; + } else { + timeDisplay = `${diffDays}天前`; + } + + return { + id: notificationData.id, + type: notificationData.category, + icon, + title: notificationData.title, + content: notificationData.content, + time: timeDisplay, + hasDetail: true, + isRead: notificationData.is_read, + created_at: notificationData.created_at, + metadata: notificationData.metadata || {}, + }; +}; diff --git a/src/store/auth/auth.slice.js b/src/store/auth/auth.slice.js new file mode 100644 index 0000000..2dc029b --- /dev/null +++ b/src/store/auth/auth.slice.js @@ -0,0 +1,72 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { checkAuthThunk, loginThunk, logoutThunk, signupThunk, updateProfileThunk } from './auth.thunk'; + +const setPending = (state) => { + state.loading = true; + state.error = null; +}; + +const setFulfilled = (state, action) => { + state.user = action.payload; + state.loading = false; + state.error = null; +}; + +const setRejected = (state, action) => { + state.error = action.payload; + state.loading = false; +}; + +const authSlice = createSlice({ + name: 'auth', + initialState: { + loading: false, + error: null, + user: null, + }, + reducers: { + login: (state, action) => { + state.user = action.payload; + }, + logout: (state) => { + state.user = null; + state.error = null; + state.loading = false; + }, + }, + extraReducers: (builder) => { + builder + .addCase(checkAuthThunk.pending, setPending) + .addCase(checkAuthThunk.fulfilled, setFulfilled) + .addCase(checkAuthThunk.rejected, setRejected) + + .addCase(loginThunk.pending, setPending) + .addCase(loginThunk.fulfilled, setFulfilled) + .addCase(loginThunk.rejected, setRejected) + + .addCase(signupThunk.pending, setPending) + .addCase(signupThunk.fulfilled, setFulfilled) + .addCase(signupThunk.rejected, setRejected) + + .addCase(updateProfileThunk.pending, setPending) + .addCase(updateProfileThunk.fulfilled, (state, action) => { + state.user = { + ...state.user, + ...action.payload, + }; + state.loading = false; + }) + .addCase(updateProfileThunk.rejected, setRejected) + + .addCase(logoutThunk.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(logoutThunk.fulfilled) + .addCase(logoutThunk.rejected, setRejected); + }, +}); + +export const { login, logout } = authSlice.actions; +const authReducer = authSlice.reducer; +export default authReducer; diff --git a/src/store/auth/auth.thunk.js b/src/store/auth/auth.thunk.js new file mode 100644 index 0000000..7f8091f --- /dev/null +++ b/src/store/auth/auth.thunk.js @@ -0,0 +1,139 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put } from '../../services/api'; +import { showNotification } from '../notification.slice'; +import { logout } from './auth.slice'; +import CryptoJS from 'crypto-js'; + +const secretKey = import.meta.env.VITE_SECRETKEY; + +export const loginThunk = createAsyncThunk( + 'auth/login', + async ({ username, password }, { rejectWithValue, dispatch }) => { + try { + const { message, data, code } = await post('/auth/login/', { username, password }); + + if (code !== 200) { + throw new Error(message); + } + if (!data) { + throw new Error(message); + } + const { token } = data; + // encrypt token + const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString(); + sessionStorage.setItem('token', encryptedToken); + + return data; + } catch (error) { + const errorMessage = error.response?.data?.message || 'Something went wrong'; + dispatch( + showNotification({ + message: errorMessage, + type: 'danger', + }) + ); + return rejectWithValue(errorMessage); + } + } +); + +export const signupThunk = createAsyncThunk('auth/signup', async (userData, { rejectWithValue, dispatch }) => { + try { + // 使用新的注册 API + const { data, message, code } = await post('/auth/register/', userData); + console.log('注册返回数据:', data); + + if (code !== 200) { + throw new Error(message); + } + // 将 token 加密存储到 sessionStorage + const { token } = data; + + if (token) { + const encryptedToken = CryptoJS.AES.encrypt(token, secretKey).toString(); + sessionStorage.setItem('token', encryptedToken); + } + + // 显示注册成功通知 + dispatch( + showNotification({ + message: '注册成功', + type: 'success', + }) + ); + + return data; + } catch (error) { + const errorMessage = error.response?.data?.message || '注册失败,请稍后重试'; + dispatch( + showNotification({ + message: errorMessage, + type: 'danger', + }) + ); + return rejectWithValue(errorMessage); + } +}); + +export const checkAuthThunk = createAsyncThunk('auth/verify', async (_, { rejectWithValue, dispatch }) => { + try { + const { data, message } = await post('/auth/verify-token/'); + const { user } = data; + if (!user) { + dispatch(logout()); + throw new Error(message || 'No token found'); + } + return user; + } catch (error) { + dispatch(logout()); + return rejectWithValue(error.response?.data?.message || 'Token verification failed'); + } +}); + +// Async thunk for logging out +export const logoutThunk = createAsyncThunk('auth/logout', async (_, { rejectWithValue, dispatch }) => { + try { + // Send the logout request to the server (this assumes your server clears any session-related info) + await post('/auth/logout/'); + dispatch(logout()); + } catch (error) { + const errorMessage = error.response?.data?.message || 'Log out failed'; + dispatch( + showNotification({ + message: errorMessage, + type: 'danger', + }) + ); + return rejectWithValue(errorMessage); + } +}); + +// 更新个人资料 +export const updateProfileThunk = createAsyncThunk('auth/updateProfile', async (userData, { rejectWithValue, dispatch }) => { + try { + const { data, message, code } = await put('/users/profile/', userData); + + if (code !== 200) { + throw new Error(message); + } + + // 显示更新成功通知 + dispatch( + showNotification({ + message: '个人信息更新成功', + type: 'success', + }) + ); + + return data; + } catch (error) { + const errorMessage = error.response?.data?.message || '更新失败,请稍后重试'; + dispatch( + showNotification({ + message: errorMessage, + type: 'danger', + }) + ); + return rejectWithValue(errorMessage); + } +}); \ No newline at end of file diff --git a/src/store/chat/chat.messages.thunks.js b/src/store/chat/chat.messages.thunks.js new file mode 100644 index 0000000..7beea0c --- /dev/null +++ b/src/store/chat/chat.messages.thunks.js @@ -0,0 +1,45 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post } from '../../services/api'; + +/** + * 获取聊天消息 + * @param {string} chatId - 聊天ID + */ +export const fetchMessages = createAsyncThunk('chat/fetchMessages', async (chatId, { rejectWithValue }) => { + try { + const response = await get(`/chat-history/${chatId}/messages/`); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data.messages; + } + + return response.data?.messages || []; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to fetch messages'); + } +}); + +/** + * 发送聊天消息 + * @param {Object} params - 消息参数 + * @param {string} params.chatId - 聊天ID + * @param {string} params.content - 消息内容 + */ +export const sendMessage = createAsyncThunk('chat/sendMessage', async ({ chatId, content }, { rejectWithValue }) => { + try { + const response = await post(`/chat-history/${chatId}/messages/`, { + content, + type: 'text', + }); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data; + } + + return response.data || {}; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to send message'); + } +}); diff --git a/src/store/chat/chat.slice.js b/src/store/chat/chat.slice.js new file mode 100644 index 0000000..890acda --- /dev/null +++ b/src/store/chat/chat.slice.js @@ -0,0 +1,354 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + fetchAvailableDatasets, + fetchChats, + createChat, + updateChat, + deleteChat, + createChatRecord, + fetchConversationDetail, + createConversation, +} from './chat.thunks'; +import { fetchMessages, sendMessage } from './chat.messages.thunks'; + +// 初始状态 +const initialState = { + // Chat history state + history: { + items: [], + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Chat session creation state + createSession: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + sessionId: null, + }, + // Chat messages state + messages: { + items: [], + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // Send message state + sendMessage: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // 可用于聊天的知识库列表 + availableDatasets: { + items: [], + status: 'idle', + error: null, + }, + // 操作状态(创建、更新、删除) + operations: { + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + // 兼容旧版本的state结构 + list: { + items: [], + total: 0, + page: 1, + page_size: 10, + status: 'idle', + error: null, + }, + // 当前聊天 + currentChat: { + data: null, + status: 'idle', + error: null, + }, +}; + +// 创建 slice +const chatSlice = createSlice({ + name: 'chat', + initialState, + reducers: { + // 重置操作状态 + resetOperationStatus: (state) => { + state.operations.status = 'idle'; + state.operations.error = null; + }, + + // 重置当前聊天 + resetCurrentChat: (state) => { + state.currentChat.data = null; + state.currentChat.status = 'idle'; + state.currentChat.error = null; + }, + + // 设置当前聊天 + setCurrentChat: (state, action) => { + state.currentChat.data = action.payload; + state.currentChat.status = 'succeeded'; + }, + + // 重置消息状态 + resetMessages: (state) => { + state.messages.items = []; + state.messages.status = 'idle'; + state.messages.error = null; + }, + + // 重置发送消息状态 + resetSendMessageStatus: (state) => { + state.sendMessage.status = 'idle'; + state.sendMessage.error = null; + }, + + // 添加消息 + addMessage: (state, action) => { + state.messages.items.push(action.payload); + }, + + // 更新消息(用于流式传输) + updateMessage: (state, action) => { + const { id, ...updates } = action.payload; + const messageIndex = state.messages.items.findIndex((msg) => msg.id === id); + + if (messageIndex !== -1) { + // 更新现有消息 + state.messages.items[messageIndex] = { + ...state.messages.items[messageIndex], + ...updates, + }; + + // 如果流式传输结束,更新发送消息状态 + if (updates.is_streaming === false) { + state.sendMessage.status = 'succeeded'; + } + } + }, + }, + extraReducers: (builder) => { + // 获取聊天列表 + builder + .addCase(fetchChats.pending, (state) => { + state.list.status = 'loading'; + state.history.status = 'loading'; + }) + .addCase(fetchChats.fulfilled, (state, action) => { + state.list.status = 'succeeded'; + + // 检查是否是追加模式 + if (action.payload.append) { + // 追加模式:将新结果添加到现有列表的前面 + state.list.items = [...action.payload.results, ...state.list.items]; + state.history.items = [...action.payload.results, ...state.history.items]; + } else { + // 替换模式:使用新结果替换整个列表 + state.list.items = action.payload.results; + state.list.total = action.payload.total; + state.list.page = action.payload.page; + state.list.page_size = action.payload.page_size; + + // 同时更新新的状态结构 + state.history.items = action.payload.results; + } + + state.history.status = 'succeeded'; + state.history.error = null; + }) + .addCase(fetchChats.rejected, (state, action) => { + state.list.status = 'failed'; + state.list.error = action.payload || action.error.message; + + // 同时更新新的状态结构 + state.history.status = 'failed'; + state.history.error = action.payload || action.error.message; + }) + + // 创建聊天 + .addCase(createChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(createChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + state.list.items.unshift(action.payload); + state.list.total += 1; + state.currentChat.data = action.payload; + state.currentChat.status = 'succeeded'; + }) + .addCase(createChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 删除聊天 + .addCase(deleteChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(deleteChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // 更新旧的状态结构 + state.list.items = state.list.items.filter((chat) => chat.id !== action.payload); + // 更新新的状态结构 + state.history.items = state.history.items.filter((chat) => chat.conversation_id !== action.payload); + + if (state.list.total > 0) { + state.list.total -= 1; + } + + if (state.currentChat.data && state.currentChat.data.id === action.payload) { + state.currentChat.data = null; + } + }) + .addCase(deleteChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 更新聊天 + .addCase(updateChat.pending, (state) => { + state.operations.status = 'loading'; + }) + .addCase(updateChat.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + const index = state.list.items.findIndex((chat) => chat.id === action.payload.id); + if (index !== -1) { + state.list.items[index] = action.payload; + } + if (state.currentChat.data && state.currentChat.data.id === action.payload.id) { + state.currentChat.data = action.payload; + } + }) + .addCase(updateChat.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.payload || action.error.message; + }) + + // 获取聊天消息 + .addCase(fetchMessages.pending, (state) => { + state.messages.status = 'loading'; + state.messages.error = null; + }) + .addCase(fetchMessages.fulfilled, (state, action) => { + state.messages.status = 'succeeded'; + state.messages.items = action.payload; + }) + .addCase(fetchMessages.rejected, (state, action) => { + state.messages.status = 'failed'; + state.messages.error = action.error.message; + }) + + // 发送聊天消息 + .addCase(sendMessage.pending, (state) => { + state.sendMessage.status = 'loading'; + state.sendMessage.error = null; + }) + .addCase(sendMessage.fulfilled, (state, action) => { + state.sendMessage.status = 'succeeded'; + // 更新消息列表 + const index = state.messages.items.findIndex( + (msg) => msg.content === action.payload.content && msg.sender === action.payload.sender + ); + if (index === -1) { + state.messages.items.push(action.payload); + } + }) + .addCase(sendMessage.rejected, (state, action) => { + state.sendMessage.status = 'failed'; + state.sendMessage.error = action.error.message; + }) + + // 处理创建聊天记录 + .addCase(createChatRecord.pending, (state) => { + state.sendMessage.status = 'loading'; + state.sendMessage.error = null; + }) + .addCase(createChatRecord.fulfilled, (state, action) => { + // 更新状态以反映聊天已创建 + if (action.payload.conversation_id && !state.currentChat.data) { + // 设置当前聊天的会话ID + state.currentChat.data = { + conversation_id: action.payload.conversation_id, + // 其他信息将由流式更新填充 + }; + } + // 不再在这里添加消息,因为消息已经在thunk函数中添加 + }) + .addCase(createChatRecord.rejected, (state, action) => { + state.sendMessage.status = 'failed'; + state.sendMessage.error = action.error.message; + }) + + // 处理创建会话 + .addCase(createConversation.pending, (state) => { + state.createSession.status = 'loading'; + state.createSession.error = null; + }) + .addCase(createConversation.fulfilled, (state, action) => { + state.createSession.status = 'succeeded'; + state.createSession.sessionId = action.payload.conversation_id; + + // 当前聊天设置 - 使用与fetchConversationDetail相同的数据结构 + state.currentChat.data = { + conversation_id: action.payload.conversation_id, + datasets: action.payload.datasets || [], + // 添加其他必要的字段,确保与fetchConversationDetail返回的数据结构兼容 + messages: [], + create_time: new Date().toISOString(), + update_time: new Date().toISOString(), + }; + state.currentChat.status = 'succeeded'; + state.currentChat.error = null; + }) + .addCase(createConversation.rejected, (state, action) => { + state.createSession.status = 'failed'; + state.createSession.error = action.payload || action.error.message; + }) + + // 处理获取可用知识库 + .addCase(fetchAvailableDatasets.pending, (state) => { + state.availableDatasets.status = 'loading'; + state.availableDatasets.error = null; + }) + .addCase(fetchAvailableDatasets.fulfilled, (state, action) => { + state.availableDatasets.status = 'succeeded'; + state.availableDatasets.items = action.payload || []; + state.availableDatasets.error = null; + }) + .addCase(fetchAvailableDatasets.rejected, (state, action) => { + state.availableDatasets.status = 'failed'; + state.availableDatasets.error = action.payload || '获取可用知识库失败'; + }) + + // 获取会话详情 + .addCase(fetchConversationDetail.pending, (state) => { + state.currentChat.status = 'loading'; + state.currentChat.error = null; + }) + .addCase(fetchConversationDetail.fulfilled, (state, action) => { + if (action.payload) { + state.currentChat.status = 'succeeded'; + state.currentChat.data = action.payload; + } else { + state.currentChat.status = 'idle'; + state.currentChat.data = null; + } + }) + .addCase(fetchConversationDetail.rejected, (state, action) => { + state.currentChat.status = 'failed'; + state.currentChat.error = action.payload || action.error.message; + }); + }, +}); + +// 导出 actions +export const { + resetOperationStatus, + resetCurrentChat, + setCurrentChat, + resetMessages, + resetSendMessageStatus, + addMessage, + updateMessage, +} = chatSlice.actions; + +// 导出 reducer +export default chatSlice.reducer; diff --git a/src/store/chat/chat.thunks.js b/src/store/chat/chat.thunks.js new file mode 100644 index 0000000..607c120 --- /dev/null +++ b/src/store/chat/chat.thunks.js @@ -0,0 +1,491 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put, del, streamRequest } from '../../services/api'; +import { showNotification } from '../notification.slice'; +import { addMessage, updateMessage, setCurrentChat } from './chat.slice'; + +/** + * 获取聊天列表 + * @param {Object} params - 查询参数 + * @param {number} params.page - 页码 + * @param {number} params.page_size - 每页数量 + */ +export const fetchChats = createAsyncThunk('chat/fetchChats', async (params = {}, { rejectWithValue }) => { + try { + const response = await get('/chat-history/', { params }); + + // 处理返回格式 + if (response && response.code === 200) { + return { + results: response.data.results, + total: response.data.total, + page: response.data.page || 1, + page_size: response.data.page_size || 10, + }; + } + + return { results: [], total: 0, page: 1, page_size: 10 }; + } catch (error) { + console.error('Error fetching chats:', error); + return rejectWithValue(error.response?.data?.message || 'Failed to fetch chats'); + } +}); + +/** + * 创建新聊天 + * @param {Object} chatData - 聊天数据 + * @param {string} chatData.knowledge_base_id - 知识库ID + * @param {string} chatData.title - 聊天标题 + */ +export const createChat = createAsyncThunk('chat/createChat', async (chatData, { rejectWithValue }) => { + try { + const response = await post('/chat-history/', chatData); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data.chat; + } + + return response.data?.chat || {}; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to create chat'); + } +}); + +/** + * 更新聊天 + * @param {Object} params - 更新参数 + * @param {string} params.id - 聊天ID + * @param {Object} params.data - 更新数据 + */ +export const updateChat = createAsyncThunk('chat/updateChat', async ({ id, data }, { rejectWithValue }) => { + try { + const response = await put(`/chat-history/${id}/`, data); + + // 处理返回格式 + if (response && response.code === 200) { + return response.data.chat; + } + + return response.data?.chat || {}; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to update chat'); + } +}); + +/** + * 删除聊天 + * @param {string} conversationId - 聊天ID + */ +export const deleteChat = createAsyncThunk('chat/deleteChat', async (conversationId, { rejectWithValue }) => { + try { + const response = await del(`/chat-history/delete_conversation?conversation_id=${conversationId}`); + + // 处理返回格式 + if (response && response.code === 200) { + return conversationId; + } + + return conversationId; + } catch (error) { + console.error('Error deleting chat:', error); + return rejectWithValue(error.response?.data?.message || '删除聊天失败'); + } +}); + +/** + * 获取可用于聊天的知识库列表 + */ +export const fetchAvailableDatasets = createAsyncThunk( + 'chat/fetchAvailableDatasets', + async (_, { rejectWithValue }) => { + try { + const response = await get('/chat-history/available_datasets/'); + + if (response && response.code === 200) { + return response.data; + } + + return rejectWithValue('获取可用知识库列表失败'); + } catch (error) { + console.error('Error fetching available datasets:', error); + return rejectWithValue(error.response?.data?.message || '获取可用知识库列表失败'); + } + } +); + +/** + * 创建聊天记录 + * @param {Object} params - 聊天参数 + * @param {string[]} params.dataset_id_list - 知识库ID列表 + * @param {string} params.question - 用户问题 + * @param {string} params.conversation_id - 会话ID,可选 + */ +export const createChatRecord = createAsyncThunk( + 'chat/createChatRecord', + async ({ question, conversation_id, dataset_id_list }, { dispatch, getState, rejectWithValue }) => { + try { + // 构建请求数据 + const requestBody = { + question, + dataset_id_list, + conversation_id, + }; + console.log('准备发送聊天请求:', requestBody); + + // 先添加用户消息到聊天窗口 + const userMessageId = Date.now().toString(); + dispatch( + addMessage({ + id: userMessageId, + role: 'user', + content: question, + created_at: new Date().toISOString(), + }) + ); + + // 添加临时的助手消息(流式传输期间显示) + const assistantMessageId = (Date.now() + 1).toString(); + dispatch( + addMessage({ + id: assistantMessageId, + role: 'assistant', + content: '', + created_at: new Date().toISOString(), + is_streaming: true, + }) + ); + + let finalMessage = ''; + let conversationId = conversation_id; + + // 使用流式请求函数处理 + const result = await streamRequest( + '/chat-history/', + requestBody, + // 处理每个数据块 + (chunkText) => { + try { + const data = JSON.parse(chunkText); + console.log('收到聊天数据块:', data); + + if (data.code === 200) { + // 保存会话ID (无论消息类型,只要找到会话ID就保存) + if (data.data && data.data.conversation_id && !conversationId) { + conversationId = data.data.conversation_id; + console.log('获取到会话ID:', conversationId); + } + + // 处理各种可能的消息类型 + const messageType = data.message; + + // 处理部分内容更新 + if ((messageType === 'partial' || messageType === '部分') && data.data) { + // 累加内容 + if (data.data.content !== undefined) { + finalMessage += data.data.content; + console.log('累加内容:', finalMessage); + + // 更新消息内容 + dispatch( + updateMessage({ + id: assistantMessageId, + content: finalMessage, + }) + ); + } + + // 处理结束标志 + if (data.data.is_end) { + console.log('检测到消息结束标志'); + dispatch( + updateMessage({ + id: assistantMessageId, + is_streaming: false, + }) + ); + } + } + // 处理开始流式传输的消息 + else if (messageType === '开始流式传输' || messageType === 'start_streaming') { + console.log('开始流式传输,会话ID:', data.data?.conversation_id); + } + // 处理完成消息 + else if ( + messageType === 'completed' || + messageType === '完成' || + messageType === 'end_streaming' || + messageType === '结束流式传输' + ) { + console.log('收到完成消息'); + dispatch( + updateMessage({ + id: assistantMessageId, + is_streaming: false, + }) + ); + } + // 其他类型的消息 + else { + console.log('收到其他类型消息:', messageType); + // 如果有content字段,也尝试更新 + if (data.data && data.data.content !== undefined) { + finalMessage += data.data.content; + dispatch( + updateMessage({ + id: assistantMessageId, + content: finalMessage, + }) + ); + } + } + } else { + console.warn('收到非成功状态码:', data.code, data.message); + } + } catch (error) { + console.error('解析或处理JSON失败:', error, '原始数据:', chunkText); + } + }, + // 处理错误 + (error) => { + console.error('流式请求错误:', error); + dispatch( + updateMessage({ + id: assistantMessageId, + content: `错误: ${error.message || '请求失败'}`, + is_streaming: false, + }) + ); + } + ); + + // 确保流式传输结束后标记消息已完成 + dispatch( + updateMessage({ + id: assistantMessageId, + is_streaming: false, + }) + ); + + // 返回会话信息 + const chatInfo = { + conversation_id: conversationId || result.conversation_id, + success: true, + }; + + // 如果聊天创建成功,添加到历史列表,并设置为当前激活聊天 + if (chatInfo.conversation_id) { + // 获取知识库信息 + const state = getState(); + const availableDatasets = state.chat.availableDatasets.items || []; + const existingChats = state.chat.history.items || []; + + // 检查是否已存在此会话ID的记录 + const existingChat = existingChats.find((chat) => chat.conversation_id === chatInfo.conversation_id); + + // 只有在不存在相同会话ID的记录时才创建新的记录 + if (!existingChat) { + console.log('创建新的聊天sidebar记录:', chatInfo.conversation_id); + + // 创建一个新的聊天记录对象添加到历史列表 + const newChatEntry = { + conversation_id: chatInfo.conversation_id, + datasets: dataset_id_list.map((id) => { + // 尝试查找知识库名称 + const formattedId = id.includes('-') + ? id + : id.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'); + const dataset = availableDatasets.find((ds) => ds.id === formattedId); + return { + id: formattedId, + name: dataset?.name || '新知识库对话', + }; + }), + create_time: new Date().toISOString(), + last_message: question, + message_count: 2, // 用户问题和助手回复 + }; + + // 更新当前聊天 + dispatch({ + type: 'chat/fetchChats/fulfilled', + payload: { + results: [newChatEntry], + total: 1, + append: true, // 标记为追加,而不是替换 + }, + }); + } else { + console.log('聊天sidebar记录已存在,不再创建:', chatInfo.conversation_id); + } + + // 设置为当前聊天 + dispatch( + setCurrentChat({ + conversation_id: chatInfo.conversation_id, + datasets: existingChat + ? existingChat.datasets + : dataset_id_list.map((id) => { + const formattedId = id.includes('-') + ? id + : id.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'); + const dataset = availableDatasets.find((ds) => ds.id === formattedId); + return { + id: formattedId, + name: dataset?.name || '新知识库对话', + }; + }), + }) + ); + } + + return chatInfo; + } catch (error) { + console.error('创建聊天记录失败:', error); + + // 显示错误通知 + dispatch( + showNotification({ + message: `发送失败: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + + return rejectWithValue(error.message || '创建聊天记录失败'); + } + } +); + +/** + * 获取会话详情 + * @param {string} conversationId - 会话ID + */ +export const fetchConversationDetail = createAsyncThunk( + 'chat/fetchConversationDetail', + async (conversationId, { rejectWithValue, dispatch, getState }) => { + try { + // 先检查是否是刚创建的会话 + const state = getState(); + const createSession = state.chat.createSession || {}; + const currentChat = state.chat.currentChat.data; + + // 如果是刚创建成功的会话,且会话ID匹配,则直接返回现有会话数据 + if ( + createSession.status === 'succeeded' && + createSession.sessionId === conversationId && + currentChat?.conversation_id === conversationId + ) { + console.log('使用新创建的会话数据,跳过详情请求:', conversationId); + return currentChat; + } + + const response = await get('/chat-history/conversation_detail', { + params: { conversation_id: conversationId }, + }); + + if (response && response.code === 200) { + // 如果存在消息,更新Redux状态 + if (response.data.messages) { + dispatch({ + type: 'chat/fetchMessages/fulfilled', + payload: response.data.messages, + }); + } + + return response.data; + } + + return rejectWithValue('获取会话详情失败'); + } catch (error) { + // 明确检查是否是404错误 + const is404Error = error.response && error.response.status === 404; + + if (is404Error) { + console.log('会话未找到,可能是新创建的会话:', conversationId); + return null; + } + + console.error('Error fetching conversation detail:', error); + return rejectWithValue(error.response?.data?.message || '获取会话详情失败'); + } + } +); + +/** + * 创建新会话(仅获取会话ID,不发送消息) + * @param {Object} params - 参数 + * @param {string[]} params.dataset_id_list - 知识库ID列表 + */ +export const createConversation = createAsyncThunk( + 'chat/createConversation', + async ({ dataset_id_list }, { dispatch, getState, rejectWithValue }) => { + try { + console.log('创建新会话,知识库ID列表:', dataset_id_list); + const params = { + dataset_id_list: dataset_id_list, + }; + const response = await post('/chat-history/create_conversation/', params); + + if (response && response.code === 200) { + const conversationData = response.data; + console.log('会话创建成功:', conversationData); + + // 获取知识库信息 + const state = getState(); + const availableDatasets = state.chat.availableDatasets.items || []; + + // 创建一个新的聊天记录对象添加到历史列表 + const newChatEntry = { + conversation_id: conversationData.conversation_id, + datasets: dataset_id_list.map((id) => { + // 尝试查找知识库名称 + const formattedId = id.includes('-') + ? id + : id.replace(/(.{8})(.{4})(.{4})(.{4})(.{12})/, '$1-$2-$3-$4-$5'); + const dataset = availableDatasets.find((ds) => ds.id === formattedId); + return { + id: formattedId, + name: dataset?.name || '新知识库对话', + }; + }), + create_time: new Date().toISOString(), + last_message: '', + message_count: 0, + }; + + // 更新聊天历史列表 + dispatch({ + type: 'chat/fetchChats/fulfilled', + payload: { + results: [newChatEntry], + total: 1, + append: true, // 标记为追加,而不是替换 + }, + }); + + // 设置为当前聊天 + dispatch( + setCurrentChat({ + conversation_id: conversationData.conversation_id, + datasets: newChatEntry.datasets, + }) + ); + + return conversationData; + } + + return rejectWithValue('创建会话失败'); + } catch (error) { + console.error('创建会话失败:', error); + + // 显示错误通知 + dispatch( + showNotification({ + message: `创建会话失败: ${error.message || '未知错误'}`, + type: 'danger', + }) + ); + + return rejectWithValue(error.message || '创建会话失败'); + } + } +); diff --git a/src/store/knowledgeBase/knowledgeBase.slice.js b/src/store/knowledgeBase/knowledgeBase.slice.js new file mode 100644 index 0000000..60d1835 --- /dev/null +++ b/src/store/knowledgeBase/knowledgeBase.slice.js @@ -0,0 +1,269 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + fetchKnowledgeBases, + createKnowledgeBase, + updateKnowledgeBase, + deleteKnowledgeBase, + changeKnowledgeBaseType, + searchKnowledgeBases, + requestKnowledgeBaseAccess, + getKnowledgeBaseById, + uploadDocument, + getKnowledgeBaseDocuments, + deleteKnowledgeBaseDocument, +} from './knowledgeBase.thunks'; + +const initialState = { + knowledgeBases: [], + currentKnowledgeBase: null, + searchResults: [], + searchLoading: false, + loading: false, + error: null, + pagination: { + total: 0, + page: 1, + page_size: 10, + total_pages: 1, + }, + batchPermissions: {}, + batchLoading: false, + editStatus: 'idle', + requestAccessStatus: 'idle', + uploadStatus: 'idle', + documents: { + items: [], + loading: false, + error: null, + pagination: { + total: 0, + page: 1, + page_size: 10, + }, + }, +}; + +const knowledgeBaseSlice = createSlice({ + name: 'knowledgeBase', + initialState, + reducers: { + clearCurrentKnowledgeBase: (state) => { + state.currentKnowledgeBase = null; + }, + clearSearchResults: (state) => { + state.searchResults = []; + }, + clearEditStatus: (state) => { + state.editStatus = 'idle'; + }, + }, + extraReducers: (builder) => { + builder + // 获取知识库列表 + .addCase(fetchKnowledgeBases.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(fetchKnowledgeBases.fulfilled, (state, action) => { + state.loading = false; + state.knowledgeBases = action.payload.items || []; + state.pagination = { + total: action.payload.total || 0, + page: action.payload.page || 1, + page_size: action.payload.page_size || 10, + total_pages: action.payload.total_pages || 1, + }; + }) + .addCase(fetchKnowledgeBases.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || 'Failed to fetch knowledge bases'; + }) + + // 创建知识库 + .addCase(createKnowledgeBase.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(createKnowledgeBase.fulfilled, (state, action) => { + state.loading = false; + state.editStatus = 'successful'; + // 不需要更新 knowledgeBases,因为创建后会跳转到详情页 + }) + .addCase(createKnowledgeBase.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || 'Failed to create knowledge base'; + state.editStatus = 'failed'; + }) + + // 更新知识库 + .addCase(updateKnowledgeBase.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(updateKnowledgeBase.fulfilled, (state, action) => { + state.loading = false; + // 只更新基本信息,保留其他属性(如permissions等) + if (state.currentKnowledgeBase) { + state.currentKnowledgeBase = { + ...state.currentKnowledgeBase, + name: action.payload.name, + desc: action.payload.desc || action.payload.description, + description: action.payload.description || action.payload.desc, + type: action.payload.type || state.currentKnowledgeBase.type, + department: action.payload.department || state.currentKnowledgeBase.department, + group: action.payload.group || state.currentKnowledgeBase.group, + }; + } + state.editStatus = 'successful'; + }) + .addCase(updateKnowledgeBase.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || 'Failed to update knowledge base'; + state.editStatus = 'failed'; + }) + + // 删除知识库 + .addCase(deleteKnowledgeBase.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(deleteKnowledgeBase.fulfilled, (state, action) => { + state.loading = false; + const deletedId = action.payload; + state.knowledgeBases = state.knowledgeBases.filter((kb) => kb.id !== deletedId); + if (state.pagination.total > 0) { + state.pagination.total -= 1; + state.pagination.total_pages = Math.ceil(state.pagination.total / state.pagination.page_size); + } + }) + .addCase(deleteKnowledgeBase.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || 'Failed to delete knowledge base'; + }) + + // 修改知识库类型 + .addCase(changeKnowledgeBaseType.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(changeKnowledgeBaseType.fulfilled, (state, action) => { + state.loading = false; + if (state.currentKnowledgeBase) { + state.currentKnowledgeBase = { + ...state.currentKnowledgeBase, + type: action.payload.type, + department: action.payload.department, + group: action.payload.group, + }; + } + state.editStatus = 'successful'; + }) + .addCase(changeKnowledgeBaseType.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || 'Failed to change knowledge base type'; + state.editStatus = 'failed'; + }) + + // 搜索知识库 + .addCase(searchKnowledgeBases.pending, (state) => { + state.searchLoading = true; + state.error = null; + }) + .addCase(searchKnowledgeBases.fulfilled, (state, action) => { + state.searchLoading = false; + if (action.payload && action.payload.code === 200) { + state.searchResults = action.payload.data.items || []; + } else { + state.searchResults = action.payload.items || []; + } + }) + .addCase(searchKnowledgeBases.rejected, (state, action) => { + state.searchLoading = false; + state.error = action.payload || 'Failed to search knowledge bases'; + }) + + // 申请知识库访问权限 + .addCase(requestKnowledgeBaseAccess.pending, (state) => { + state.requestAccessStatus = 'loading'; + }) + .addCase(requestKnowledgeBaseAccess.fulfilled, (state) => { + state.requestAccessStatus = 'successful'; + }) + .addCase(requestKnowledgeBaseAccess.rejected, (state) => { + state.requestAccessStatus = 'failed'; + }) + + // 获取知识库详情 + .addCase(getKnowledgeBaseById.pending, (state) => { + state.loading = true; + state.error = null; + }) + .addCase(getKnowledgeBaseById.fulfilled, (state, action) => { + state.loading = false; + state.currentKnowledgeBase = action.payload.knowledge_base || action.payload; + }) + .addCase(getKnowledgeBaseById.rejected, (state, action) => { + state.loading = false; + state.error = action.payload || 'Failed to get knowledge base details'; + }) + + // 上传文档 + .addCase(uploadDocument.pending, (state) => { + state.uploadStatus = 'loading'; + }) + .addCase(uploadDocument.fulfilled, (state) => { + state.uploadStatus = 'successful'; + }) + .addCase(uploadDocument.rejected, (state, action) => { + state.uploadStatus = 'failed'; + state.error = action.payload || 'Failed to upload document'; + }) + + // 获取知识库文档列表 + .addCase(getKnowledgeBaseDocuments.pending, (state) => { + state.documents.loading = true; + state.documents.error = null; + }) + .addCase(getKnowledgeBaseDocuments.fulfilled, (state, action) => { + state.documents.loading = false; + state.documents.items = action.payload.items || []; + state.documents.pagination = { + total: action.payload.total || 0, + page: 1, + page_size: 10, + }; + console.log('文档数据已更新到store:', { + itemsCount: state.documents.items.length, + items: state.documents.items, + }); + }) + .addCase(getKnowledgeBaseDocuments.rejected, (state, action) => { + state.documents.loading = false; + state.documents.error = action.payload || 'Failed to get documents'; + }) + + // 删除知识库文档 + .addCase(deleteKnowledgeBaseDocument.pending, (state) => { + state.documents.loading = true; + state.documents.error = null; + }) + .addCase(deleteKnowledgeBaseDocument.fulfilled, (state, action) => { + state.documents.loading = false; + const deletedDocId = action.payload; + state.documents.items = state.documents.items.filter( + (doc) => doc.document_id !== deletedDocId + ); + if (state.documents.pagination.total > 0) { + state.documents.pagination.total -= 1; + } + }) + .addCase(deleteKnowledgeBaseDocument.rejected, (state, action) => { + state.documents.loading = false; + state.documents.error = action.payload || 'Failed to delete document'; + }); + }, +}); + +export const { clearCurrentKnowledgeBase, clearSearchResults, clearEditStatus } = knowledgeBaseSlice.actions; + +export default knowledgeBaseSlice.reducer; diff --git a/src/store/knowledgeBase/knowledgeBase.thunks.js b/src/store/knowledgeBase/knowledgeBase.thunks.js new file mode 100644 index 0000000..62bf221 --- /dev/null +++ b/src/store/knowledgeBase/knowledgeBase.thunks.js @@ -0,0 +1,333 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put, del, upload } from '../../services/api'; +import { showNotification } from '../notification.slice'; + +/** + * Fetch knowledge bases with pagination + * @param {Object} params - Pagination parameters + * @param {number} params.page - Page number (default: 1) + * @param {number} params.page_size - Page size (default: 10) + */ +export const fetchKnowledgeBases = createAsyncThunk( + 'knowledgeBase/fetchKnowledgeBases', + async ({ page = 1, page_size = 10 } = {}, { rejectWithValue }) => { + try { + const response = await get('/knowledge-bases', { params: { page, page_size } }); + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data; + } + return response.data; + } catch (error) { + console.log(error); + + return rejectWithValue(error.response?.data.error.message || 'Failed to fetch knowledge bases'); + } + } +); + +/** + * Search knowledge bases + * @param {Object} params - Search parameters + * @param {string} params.keyword - Search keyword + * @param {number} params.page - Page number (default: 1) + * @param {number} params.page_size - Page size (default: 10) + */ +export const searchKnowledgeBases = createAsyncThunk('knowledgeBase/search', async (params, { rejectWithValue }) => { + try { + const { keyword, page = 1, page_size = 10 } = params; + const response = await get('/knowledge-bases/search', { + params: { + keyword, + page, + page_size, + }, + }); + + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data; + } + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data?.message || error.message); + } +}); + +/** + * Create a new knowledge base + */ +export const createKnowledgeBase = createAsyncThunk( + 'knowledgeBase/createKnowledgeBase', + async (knowledgeBaseData, { rejectWithValue }) => { + try { + const response = await post('/knowledge-bases/', knowledgeBaseData); + + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data.knowledge_base; + } + + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to create knowledge base'); + } + } +); + +/** + * Get knowledge base details by ID + */ +export const getKnowledgeBaseById = createAsyncThunk( + 'knowledgeBase/getKnowledgeBaseById', + async (id, { rejectWithValue }) => { + try { + const response = await get(`/knowledge-bases/${id}/`); + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data; + } + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to get knowledge base details'); + } + } +); + +/** + * Update knowledge base + * @param {Object} params - Update parameters + * @param {string} params.id - Knowledge base ID + * @param {Object} params.data - Update data (name, desc) + */ +export const updateKnowledgeBase = createAsyncThunk( + 'knowledgeBase/updateKnowledgeBase', + async ({ id, data }, { rejectWithValue }) => { + try { + const response = await put(`/knowledge-bases/${id}/`, data); + + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data.knowledge_base; + } + + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to update knowledge base'); + } + } +); + +/** + * Delete knowledge base + * @param {string} id - Knowledge base ID + */ +export const deleteKnowledgeBase = createAsyncThunk( + 'knowledgeBase/deleteKnowledgeBase', + async (id, { rejectWithValue }) => { + try { + await del(`/knowledge-bases/${id}/`); + return id; + } catch (error) { + return rejectWithValue(error.response?.data?.message || 'Failed to delete knowledge base'); + } + } +); + +/** + * Change knowledge base type + * @param {Object} params - Parameters + * @param {string} params.id - Knowledge base ID + * @param {string} params.type - New knowledge base type + * @param {string} params.department - User department + * @param {string} params.group - User group + */ +export const changeKnowledgeBaseType = createAsyncThunk( + 'knowledgeBase/changeType', + async ({ id, type, department, group }, { rejectWithValue }) => { + try { + console.log(id, type, department, group); + + const response = await post(`/knowledge-bases/${id}/change_type/`, { + type, + department, + group, + }); + + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + return response.data.data; + } + + return response.data; + } catch (error) { + return rejectWithValue(error.response?.data?.message || '修改知识库类型失败'); + } + } +); + +/** + * 申请知识库访问权限 + * @param {Object} params - 参数 + * @param {string} params.knowledge_base - 知识库ID + * @param {Object} params.permissions - 权限对象 + * @param {string} params.reason - 申请原因 + * @param {string} params.expires_at - 过期时间 + * @returns {Promise} - Promise对象 + */ +export const requestKnowledgeBaseAccess = createAsyncThunk( + 'knowledgeBase/requestAccess', + async (params, { rejectWithValue, dispatch }) => { + try { + const response = await post('/permissions/', params); + dispatch( + showNotification({ + type: 'success', + message: '权限申请已发送,请等待管理员审核', + }) + ); + return response.data; + } catch (error) { + dispatch( + showNotification({ + type: 'danger', + message: error.response?.data?.detail || '权限申请失败,请稍后重试', + }) + ); + return rejectWithValue(error.response?.data?.message || error.message); + } + } +); + +/** + * Upload documents to a knowledge base + * @param {Object} params - Upload parameters + * @param {string} params.knowledge_base_id - Knowledge base ID + * @param {File[]} params.files - Files to upload + */ +export const uploadDocument = createAsyncThunk( + 'knowledgeBase/uploadDocument', + async ({ knowledge_base_id, files }, { rejectWithValue, dispatch }) => { + try { + const formData = new FormData(); + + // 支持单文件和多文件上传 + if (Array.isArray(files)) { + // 多文件上传 + files.forEach(file => { + formData.append('files', file); + }); + } else { + // 单文件上传(向后兼容) + formData.append('files', files); + } + + const response = await post(`/knowledge-bases/${knowledge_base_id}/upload_document/`, formData, true); + + // 处理新的返回格式 + if (response.data && response.data.code === 200) { + const result = response.data.data; + + // 使用API返回的消息作为通知 + dispatch( + showNotification({ + type: 'success', + message: response.data.message || `文档上传完成,成功: ${result.uploaded_count},失败: ${result.failed_count}`, + }) + ); + + return result; + } + + dispatch( + showNotification({ + type: 'success', + message: Array.isArray(files) + ? `${files.length} 个文档上传成功` + : `文档 ${files.name} 上传成功`, + }) + ); + + return response.data; + } catch (error) { + const errorMessage = error.response?.data?.message || error.message || '文档上传失败'; + dispatch( + showNotification({ + type: 'danger', + message: errorMessage, + }) + ); + return rejectWithValue(errorMessage); + } + } +); + +/** + * Get documents list for a knowledge base + * @param {Object} params - Parameters + * @param {string} params.knowledge_base_id - Knowledge base ID + */ +export const getKnowledgeBaseDocuments = createAsyncThunk( + 'knowledgeBase/getDocuments', + async ({ knowledge_base_id }, { rejectWithValue }) => { + try { + console.log('获取知识库文档列表:', knowledge_base_id); + const { data, code } = await get(`/knowledge-bases/${knowledge_base_id}/documents`); + console.log('文档列表API响应:', { data, code }); + + // 处理返回格式 + if (code === 200) { + console.log('API返回数据:', data); + return { + items: data || [], + total: (data || []).length + }; + } else { + // 未知格式,尝试提取数据 + const items = data?.items || data || []; + console.log('未识别格式,提取数据:', items); + return { + items: items, + total: items.length + }; + } + } catch (error) { + console.error('获取知识库文档失败:', error); + return rejectWithValue(error.response?.data?.message || '获取文档列表失败'); + } + } +); + +/** + * Delete a document from a knowledge base + * @param {Object} params - Parameters + * @param {string} params.knowledge_base_id - Knowledge base ID + * @param {string} params.document_id - Document ID + */ +export const deleteKnowledgeBaseDocument = createAsyncThunk( + 'knowledgeBase/deleteDocument', + async ({ knowledge_base_id, document_id }, { rejectWithValue, dispatch }) => { + try { + console.log(knowledge_base_id, document_id); + + await del(`/knowledge-bases/${knowledge_base_id}/delete_document?document_id=${document_id}`); + + dispatch( + showNotification({ + type: 'success', + message: '文档删除成功', + }) + ); + + return document_id; + } catch (error) { + dispatch( + showNotification({ + type: 'danger', + message: error.response?.data?.message || '文档删除失败', + }) + ); + return rejectWithValue(error.response?.data?.message || '文档删除失败'); + } + } +); diff --git a/src/store/notification.slice.js b/src/store/notification.slice.js new file mode 100644 index 0000000..b0658cd --- /dev/null +++ b/src/store/notification.slice.js @@ -0,0 +1,14 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const notificationSlice = createSlice({ + name: 'notification', + initialState: null, // type(success/primary/warning/danger), message, duration + reducers: { + showNotification: (state, action) => action.payload, + hideNotification: () => null, + }, +}); + +export const { showNotification, hideNotification } = notificationSlice.actions; +const notificationReducer = notificationSlice.reducer; +export default notificationReducer; diff --git a/src/store/notificationCenter/notificationCenter.slice.js b/src/store/notificationCenter/notificationCenter.slice.js new file mode 100644 index 0000000..9ccce4b --- /dev/null +++ b/src/store/notificationCenter/notificationCenter.slice.js @@ -0,0 +1,123 @@ +import { createSlice } from '@reduxjs/toolkit'; + +const initialState = { + notifications: [ + { + id: 1, + type: 'permission', + icon: 'bi-shield', + title: '新的权限请求', + content: '张三请求访问销售数据集', + time: '10分钟前', + hasDetail: true, + isRead: false, + }, + { + id: 2, + type: 'system', + icon: 'bi-info-circle', + title: '系统更新通知', + content: '系统将在今晚23:00进行例行维护', + time: '1小时前', + hasDetail: false, + isRead: false, + }, + { + id: 3, + type: 'permission', + icon: 'bi-shield', + title: '新的权限请求', + content: '李四请求访问用户数据集', + time: '2小时前', + hasDetail: true, + isRead: false, + }, + { + id: 4, + type: 'system', + icon: 'bi-exclamation-circle', + title: '安全提醒', + content: '检测到异常登录行为,请及时查看', + time: '3小时前', + hasDetail: true, + isRead: false, + }, + { + id: 5, + type: 'permission', + icon: 'bi-shield', + title: '权限变更通知', + content: '管理员修改了您的数据访问权限', + time: '1天前', + hasDetail: true, + isRead: false, + }, + ], + unreadCount: 5, + isConnected: false, +}; + +const notificationCenterSlice = createSlice({ + name: 'notificationCenter', + initialState, + reducers: { + clearNotifications: (state) => { + state.notifications = []; + state.unreadCount = 0; + }, + + addNotification: (state, action) => { + // 检查通知是否已存在 + const exists = state.notifications.some((n) => n.id === action.payload.id); + if (!exists) { + // 将新通知添加到列表的开头 + state.notifications.unshift(action.payload); + // 如果通知未读,增加未读计数 + if (!action.payload.isRead) { + state.unreadCount += 1; + } + } + }, + + markNotificationAsRead: (state, action) => { + const notification = state.notifications.find((n) => n.id === action.payload); + if (notification && !notification.isRead) { + notification.isRead = true; + state.unreadCount = Math.max(0, state.unreadCount - 1); + } + }, + + markAllNotificationsAsRead: (state) => { + state.notifications.forEach((notification) => { + notification.isRead = true; + }); + state.unreadCount = 0; + }, + + setWebSocketConnected: (state, action) => { + state.isConnected = action.payload; + }, + + removeNotification: (state, action) => { + const notificationIndex = state.notifications.findIndex((n) => n.id === action.payload); + if (notificationIndex !== -1) { + const notification = state.notifications[notificationIndex]; + if (!notification.isRead) { + state.unreadCount = Math.max(0, state.unreadCount - 1); + } + state.notifications.splice(notificationIndex, 1); + } + }, + }, +}); + +export const { + clearNotifications, + addNotification, + markNotificationAsRead, + markAllNotificationsAsRead, + setWebSocketConnected, + removeNotification, +} = notificationCenterSlice.actions; + +export default notificationCenterSlice.reducer; diff --git a/src/store/permissions/permissions.slice.js b/src/store/permissions/permissions.slice.js new file mode 100644 index 0000000..5a2a371 --- /dev/null +++ b/src/store/permissions/permissions.slice.js @@ -0,0 +1,159 @@ +import { createSlice } from '@reduxjs/toolkit'; +import { + fetchUserPermissions, + updateUserPermissions, + fetchPermissionsThunk, + approvePermissionThunk, + rejectPermissionThunk, + fetchAllUserPermissions, +} from './permissions.thunks'; + +const initialState = { + users: { + items: [], + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + allUsersPermissions: { + results: [], + total: 0, + page: 1, + page_size: 10, + status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed' + error: null, + }, + operations: { + status: 'idle', + error: null, + }, + pending: { + results: [], // 更改为results + total: 0, // 添加total + page: 1, // 添加page + page_size: 10, // 添加page_size + status: 'idle', + error: null, + }, + approveReject: { + status: 'idle', + error: null, + currentId: null, + }, +}; + +const permissionsSlice = createSlice({ + name: 'permissions', + initialState, + reducers: { + resetOperationStatus: (state) => { + state.operations.status = 'idle'; + state.operations.error = null; + }, + }, + extraReducers: (builder) => { + builder + // 获取用户权限列表 + .addCase(fetchUserPermissions.pending, (state) => { + state.users.status = 'loading'; + state.users.error = null; + }) + .addCase(fetchUserPermissions.fulfilled, (state, action) => { + state.users.status = 'succeeded'; + state.users.items = action.payload; + }) + .addCase(fetchUserPermissions.rejected, (state, action) => { + state.users.status = 'failed'; + state.users.error = action.error.message; + }) + + // 更新用户权限 + .addCase(updateUserPermissions.pending, (state) => { + state.operations.status = 'loading'; + state.operations.error = null; + }) + .addCase(updateUserPermissions.fulfilled, (state, action) => { + state.operations.status = 'succeeded'; + // 更新用户列表中的权限信息 + const index = state.users.items.findIndex((user) => user.id === action.payload.userId); + if (index !== -1) { + state.users.items[index] = { + ...state.users.items[index], + permissions: action.payload.permissions, + }; + } + }) + .addCase(updateUserPermissions.rejected, (state, action) => { + state.operations.status = 'failed'; + state.operations.error = action.error.message; + }) + + // 获取待处理申请列表 + .addCase(fetchPermissionsThunk.pending, (state) => { + state.pending.status = 'loading'; + state.pending.error = null; + }) + .addCase(fetchPermissionsThunk.fulfilled, (state, action) => { + state.pending.status = 'succeeded'; + state.pending.results = action.payload.results || []; + state.pending.total = action.payload.total || 0; + state.pending.page = action.payload.page || 1; + state.pending.page_size = action.payload.page_size || 10; + }) + .addCase(fetchPermissionsThunk.rejected, (state, action) => { + state.pending.status = 'failed'; + state.pending.error = action.error.message; + }) + + // 批准/拒绝权限申请 + .addCase(approvePermissionThunk.pending, (state, action) => { + state.approveReject.status = 'loading'; + state.approveReject.error = null; + state.approveReject.currentId = action.meta.arg.id; + }) + .addCase(approvePermissionThunk.fulfilled, (state) => { + state.approveReject.status = 'succeeded'; + state.approveReject.currentId = null; + }) + .addCase(approvePermissionThunk.rejected, (state, action) => { + state.approveReject.status = 'failed'; + state.approveReject.error = action.error.message; + state.approveReject.currentId = null; + }) + + .addCase(rejectPermissionThunk.pending, (state, action) => { + state.approveReject.status = 'loading'; + state.approveReject.error = null; + state.approveReject.currentId = action.meta.arg.id; + }) + .addCase(rejectPermissionThunk.fulfilled, (state) => { + state.approveReject.status = 'succeeded'; + state.approveReject.currentId = null; + }) + .addCase(rejectPermissionThunk.rejected, (state, action) => { + state.approveReject.status = 'failed'; + state.approveReject.error = action.error.message; + state.approveReject.currentId = null; + }) + + // 获取所有用户及其权限列表 + .addCase(fetchAllUserPermissions.pending, (state) => { + state.allUsersPermissions.status = 'loading'; + state.allUsersPermissions.error = null; + }) + .addCase(fetchAllUserPermissions.fulfilled, (state, action) => { + state.allUsersPermissions.status = 'succeeded'; + state.allUsersPermissions.results = action.payload.results || []; + state.allUsersPermissions.total = action.payload.total || 0; + state.allUsersPermissions.page = action.payload.page || 1; + state.allUsersPermissions.page_size = action.payload.page_size || 10; + }) + .addCase(fetchAllUserPermissions.rejected, (state, action) => { + state.allUsersPermissions.status = 'failed'; + state.allUsersPermissions.error = action.payload || action.error.message; + }); + }, +}); + +export const { resetOperationStatus } = permissionsSlice.actions; + +export default permissionsSlice.reducer; diff --git a/src/store/permissions/permissions.thunks.js b/src/store/permissions/permissions.thunks.js new file mode 100644 index 0000000..67c4d0d --- /dev/null +++ b/src/store/permissions/permissions.thunks.js @@ -0,0 +1,167 @@ +import { createAsyncThunk } from '@reduxjs/toolkit'; +import { get, post, put } from '../../services/api'; +import { showNotification } from '../notification.slice'; + +// 获取权限申请列表 +export const fetchPermissionsThunk = createAsyncThunk( + 'permissions/fetchPermissions', + async (params = {}, { rejectWithValue }) => { + try { + const response = await get('/permissions/', { params }); + + if (response && response.code === 200) { + return { + results: response.data.results || [], + total: response.data.total || 0, + page: response.data.page || 1, + page_size: response.data.page_size || 10, + }; + } + return rejectWithValue('获取权限申请列表失败'); + } catch (error) { + console.error('获取权限申请列表失败:', error); + return rejectWithValue(error.response?.data?.message || '获取权限申请列表失败'); + } + } +); + +// 批准权限申请 +export const approvePermissionThunk = createAsyncThunk( + 'permissions/approvePermission', + async ({ id, responseMessage }, { rejectWithValue }) => { + try { + const response = await post(`/permissions/${id}/approve/`, { + response_message: responseMessage || '已批准', + }); + return response; + } catch (error) { + console.error('批准权限申请失败:', error); + return rejectWithValue('批准权限申请失败'); + } + } +); + +// 拒绝权限申请 +export const rejectPermissionThunk = createAsyncThunk( + 'permissions/rejectPermission', + async ({ id, responseMessage }, { rejectWithValue }) => { + try { + const response = await post(`/permissions/${id}/reject/`, { + response_message: responseMessage || '已拒绝', + }); + return response; + } catch (error) { + console.error('拒绝权限申请失败:', error); + return rejectWithValue('拒绝权限申请失败'); + } + } +); + +// 生成模拟数据 +const generateMockUsers = () => { + const users = []; + const userNames = [ + { username: 'zhangsan', name: '张三', department: '达人组', position: '达人对接' }, + { username: 'lisi', name: '李四', department: '达人组', position: '达人对接' }, + { username: 'wangwu', name: '王五', department: '达人组', position: '达人对接' }, + { username: 'zhaoliu', name: '赵六', department: '达人组', position: '达人对接' }, + { username: 'qianqi', name: '钱七', department: '达人组', position: '达人对接' }, + { username: 'sunba', name: '孙八', department: '达人组', position: '达人对接' }, + { username: 'zhoujiu', name: '周九', department: '达人组', position: '达人对接' }, + { username: 'wushi', name: '吴十', department: '达人组', position: '达人对接' }, + ]; + + for (let i = 1; i <= 20; i++) { + const randomUser = userNames[Math.floor(Math.random() * userNames.length)]; + const hasAdminPermission = Math.random() > 0.8; // 20%的概率有管理员权限 + const hasEditPermission = Math.random() > 0.5; // 50%的概率有编辑权限 + const hasReadPermission = hasAdminPermission || hasEditPermission || Math.random() > 0.3; // 如果有管理员或编辑权限,一定有读取权限 + + users.push({ + id: i.toString(), + username: randomUser.username, + name: randomUser.name, + department: randomUser.department, + position: randomUser.position, + permissions_count: { + read: hasReadPermission ? 1 : 0, + edit: hasEditPermission ? 1 : 0, + admin: hasAdminPermission ? 1 : 0, + }, + }); + } + + return users; +}; + +// 获取用户权限列表 +export const fetchUserPermissions = createAsyncThunk( + 'permissions/fetchUserPermissions', + async (_, { rejectWithValue }) => { + try { + // 模拟API延迟 + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // 返回模拟数据 + return generateMockUsers(); + } catch (error) { + return rejectWithValue(error.message); + } + } +); + +// 更新用户权限 +export const updateUserPermissions = createAsyncThunk( + 'permissions/updateUserPermissions', + async ({ userId, permissions }, { dispatch, rejectWithValue }) => { + try { + const response = await put(`/users/${userId}/permissions/`, { permissions }); + + if (response && response.code === 200) { + dispatch( + showNotification({ + message: '权限更新成功', + type: 'success', + }) + ); + return { + userId, + permissions: response.data.permissions, + }; + } + return rejectWithValue(response?.message || '更新权限失败'); + } catch (error) { + dispatch( + showNotification({ + message: error.message || '更新权限失败', + type: 'danger', + }) + ); + return rejectWithValue(error.message); + } + } +); + +// 获取所有用户及其权限列表 +export const fetchAllUserPermissions = createAsyncThunk( + 'permissions/fetchAllUserPermissions', + async (params = {}, { rejectWithValue }) => { + try { + const response = await get('/permissions/all_permissions/', { params }); + + if (response && response.code === 200) { + return { + total: response.data.total, + page: response.data.page, + page_size: response.data.page_size, + results: response.data.results, + }; + } + + return rejectWithValue('获取用户权限列表失败'); + } catch (error) { + console.error('获取用户权限列表失败:', error); + return rejectWithValue(error.response?.data?.message || '获取用户权限列表失败'); + } + } +); diff --git a/src/store/store.js b/src/store/store.js new file mode 100644 index 0000000..8cdecb4 --- /dev/null +++ b/src/store/store.js @@ -0,0 +1,41 @@ +import { combineReducers, configureStore } from '@reduxjs/toolkit'; +import { persistReducer, persistStore } from 'redux-persist'; +import sessionStorage from 'redux-persist/lib/storage/session'; +import notificationReducer from './notification.slice.js'; +import authReducer from './auth/auth.slice.js'; +import knowledgeBaseReducer from './knowledgeBase/knowledgeBase.slice.js'; +import chatReducer from './chat/chat.slice.js'; +import permissionsReducer from './permissions/permissions.slice.js'; +import notificationCenterReducer from './notificationCenter/notificationCenter.slice.js'; + +const rootRducer = combineReducers({ + auth: authReducer, + notification: notificationReducer, + knowledgeBase: knowledgeBaseReducer, + chat: chatReducer, + permissions: permissionsReducer, + notificationCenter: notificationCenterReducer, +}); + +const persistConfig = { + key: 'root', + storage: sessionStorage, + whitelist: ['auth'], +}; + +// Persist configuration +const persistedReducer = persistReducer(persistConfig, rootRducer); + +const store = configureStore({ + reducer: persistedReducer, + middleware: (getDefaultMiddleware) => + getDefaultMiddleware({ + serializableCheck: false, // Disable serializable check for redux-persist + }), + devTools: true, +}); + +// Create the persistor to manage rehydrating the store +export const persistor = persistStore(store); + +export default store; diff --git a/src/styles/base.scss b/src/styles/base.scss new file mode 100644 index 0000000..257221e --- /dev/null +++ b/src/styles/base.scss @@ -0,0 +1,12 @@ +// Import all of Bootstrap's CSS +@import 'bootstrap/scss/bootstrap'; + +ul, +li { + padding: 0; + margin: 0; + list-style: none; +} +a { + text-decoration: none; +} diff --git a/src/styles/style.scss b/src/styles/style.scss new file mode 100644 index 0000000..7cd7833 --- /dev/null +++ b/src/styles/style.scss @@ -0,0 +1,446 @@ +@import 'bootstrap/scss/bootstrap'; + +#root { + min-width: 24rem; +} +.dropdown-toggle { + outline: 0; +} + +.snackbar { + top: 6.5rem; + z-index: 9999; +} + +/* Markdown styling in chat messages */ +.markdown-content { + font-size: 0.95rem; + line-height: 1.6; + color: inherit; + + /* Heading styles */ + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 1rem; + margin-bottom: 0.75rem; + font-weight: 600; + } + + h1 { + font-size: 1.5rem; + } + h2 { + font-size: 1.35rem; + } + h3 { + font-size: 1.2rem; + } + h4 { + font-size: 1.1rem; + } + h5, + h6 { + font-size: 1rem; + } + + /* Paragraph spacing */ + p { + margin-bottom: 0.75rem; + } + + /* Lists */ + ul, + ol { + padding-left: 1.5rem; + margin-bottom: 0.75rem; + } + + /* Code blocks with syntax highlighting */ + pre, + pre.prism-code { + margin: 0.5rem 0 !important; + padding: 0.75rem !important; + border-radius: 0.375rem !important; + font-size: 0.85rem !important; + line-height: 1.5 !important; + + /* Improve readability on dark background */ + code span { + font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace !important; + } + } + + /* Add copy button positioning for future enhancements */ + .code-block-container { + position: relative; + margin: 0.75rem 0; + } + + /* Inline code */ + code { + background-color: rgba(0, 0, 0, 0.05); + padding: 0.15rem 0.25rem; + border-radius: 0.25rem; + font-size: 0.9rem; + } + + /* Block quotes */ + blockquote { + border-left: 3px solid #dee2e6; + padding-left: 1rem; + margin-left: 0; + color: #6c757d; + } + + /* Links */ + a { + color: #0d6efd; + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + + /* Tables */ + table { + width: 100%; + margin-bottom: 0.75rem; + border-collapse: collapse; + + th, + td { + padding: 0.5rem; + border: 1px solid #dee2e6; + } + + th { + background-color: #f8f9fa; + } + } + + /* Images */ + img { + max-width: 100%; + height: auto; + border-radius: 0.25rem; + } + + /* Horizontal rule */ + hr { + margin: 1rem 0; + border: 0; + border-top: 1px solid #dee2e6; + } +} + +/* Apply different text colors based on message background */ +.bg-dark .markdown-content { + color: white; + + code { + background-color: rgba(255, 255, 255, 0.1); + } + + pre { + background-color: rgba(255, 255, 255, 0.1); + } + + blockquote { + border-left-color: rgba(255, 255, 255, 0.3); + color: rgba(255, 255, 255, 0.8); + } + + a { + color: #8bb9fe; + } +} + +.knowledge-card { + min-width: 20rem; + cursor: pointer; + + .hoverdown:hover .hoverdown-menu { + display: block; + color: red; + } + .hoverdown { + position: relative; + .hoverdown-menu { + display: none; + position: absolute; + z-index: 1; + .hoverdown-item { + width: max-content; + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + border-radius: 4px; + color: $dark; + + &:hover { + background-color: $gray-100; + } + } + } + } +} + +.auth-form { + input { + min-width: 300px !important; + } +} + +/* 自定义黑色系开关样式 */ +.dark-switch .form-check-input { + border: 1px solid #dee2e6; + background-color: #fff; /* 关闭状态背景色 */ +} + +/* 关闭状态滑块 */ +.dark-switch .form-check-input:not(:checked) { + background-image: url("data:image/svg+xml,"); +} + +/* 打开状态 */ +.dark-switch .form-check-input:checked { + background-color: #000; /* 打开状态背景色 */ + border-color: #000; +} + +/* 打开状态滑块 */ +.dark-switch .form-check-input:checked { + background-image: url("data:image/svg+xml,"); +} + +/* 悬停效果 */ +.dark-switch .form-check-input:hover { + filter: brightness(0.9); +} + +/* 禁用状态 */ +.dark-switch .form-check-input:disabled { + opacity: 0.5; + background-color: #e9ecef; +} + +// 通知中心样式 +.notification-item { + transition: background-color 0.2s ease; + + &:hover { + background-color: $gray-100; + } +} + +// 黑色主题的开关按钮 +.form-check-input:checked { + background-color: $dark; + border-color: $dark; +} + +/* 自定义分页样式 */ +.dark-pagination { + margin: 0; +} + +.dark-pagination .page-link { + color: #000; /* 默认文字颜色 */ + background-color: #fff; /* 默认背景 */ + border: 1px solid #dee2e6; /* 边框颜色 */ + transition: all 0.3s ease; /* 平滑过渡效果 */ +} + +/* 激活状态 */ +.dark-pagination .page-item.active .page-link { + background-color: #000 !important; + border-color: #000; + color: #fff !important; +} + +/* 悬停状态 */ +.dark-pagination .page-link:hover { + background-color: #f8f9fa; /* 浅灰背景 */ + border-color: #adb5bd; +} + +/* 禁用状态 */ +.dark-pagination .page-item.disabled .page-link { + color: #6c757d !important; + background-color: #e9ecef !important; + border-color: #dee2e6; + pointer-events: none; + opacity: 0.7; +} + +/* 自定义下拉框 */ +.dark-select { + border: 1px solid #000 !important; + color: #000 !important; +} + +.dark-select:focus { + box-shadow: 0 0 0 0.25rem rgba(0, 0, 0, 0.25); /* 黑色聚焦阴影 */ +} + +/* 下拉箭头颜色 */ +.dark-select { + background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='%23000' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e"); +} + +/* Code Block Styles */ +.code-block-container { + position: relative; + margin: 0.75rem 0; + border-radius: 0.375rem; + overflow: hidden; + background-color: #282c34; /* Dark background matching atomDark theme */ +} + +.code-block-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.375rem 0.75rem; + background-color: rgba(0, 0, 0, 0.2); + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.code-language-badge { + font-size: 0.75rem; + color: rgba(255, 255, 255, 0.7); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.copy-button { + background: none; + border: none; + color: rgba(255, 255, 255, 0.7); + cursor: pointer; + padding: 0.125rem 0.375rem; + font-size: 0.75rem; + display: flex; + align-items: center; + gap: 0.25rem; + border-radius: 0.25rem; + transition: all 0.2s; + + &:hover { + color: rgba(255, 255, 255, 0.9); + background-color: rgba(255, 255, 255, 0.1); + } +} + +.copied-indicator { + color: #10b981; /* Green color for success */ + display: flex; + align-items: center; + gap: 0.25rem; +} + +/* Override the default SyntaxHighlighter styles */ +.code-block-container pre { + margin: 0 !important; + border-radius: 0 !important; /* Remove rounded corners inside the container */ +} + +/* Markdown fallback styling */ +.markdown-fallback { + font-size: 0.95rem; + + .text-danger { + font-weight: 500; + } + + pre { + white-space: pre-wrap; + word-break: break-word; + background-color: rgba(0, 0, 0, 0.05); + padding: 0.75rem; + border-radius: 0.375rem; + margin-top: 0.5rem; + } +} + +/* Streaming message indicator */ +.streaming-indicator { + display: inline-flex; + align-items: center; + margin-left: 5px; + + .dot { + width: 6px; + height: 6px; + background-color: #6c757d; + border-radius: 50%; + margin: 0 2px; + animation: pulse 1.5s infinite ease-in-out; + + &.dot1 { + animation-delay: 0s; + } + + &.dot2 { + animation-delay: 0.3s; + } + + &.dot3 { + animation-delay: 0.6s; + } + } + + @keyframes pulse { + 0%, + 100% { + transform: scale(0.8); + opacity: 0.5; + } + 50% { + transform: scale(1.2); + opacity: 1; + } + } +} + +// SearchBar component styles +.search-input-group { + border: 1px solid #ced4da; + overflow: hidden; + + &.rounded-pill { + border-radius: 50rem; + } + + .search-input { + border: none; + box-shadow: none; + + &:focus { + box-shadow: none; + } + } + + .btn { + border: none; + background-color: transparent; + color: #6c757d; + + &:hover, + &:active, + &:focus { + background-color: transparent; + color: #495057; + } + } + + .search-button { + padding-left: 0.75rem; + padding-right: 0.75rem; + } +} diff --git a/src/utils/dateUtils.js b/src/utils/dateUtils.js new file mode 100644 index 0000000..9fd7a4a --- /dev/null +++ b/src/utils/dateUtils.js @@ -0,0 +1,48 @@ +/** + * 格式化日期时间 + * @param {string} dateString - 日期字符串 + * @returns {string} 格式化后的日期字符串 + */ +export const formatDate = (dateString) => { + if (!dateString) return '-'; + + const date = new Date(dateString); + + // 检查日期是否有效 + if (isNaN(date.getTime())) { + return dateString; + } + + // 格式化为 YYYY-MM-DD HH:MM:SS + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +}; + +/** + * 格式化日期(仅日期部分) + * @param {string} dateString - 日期字符串 + * @returns {string} 格式化后的日期字符串 + */ +export const formatDateOnly = (dateString) => { + if (!dateString) return '-'; + + const date = new Date(dateString); + + // 检查日期是否有效 + if (isNaN(date.getTime())) { + return dateString; + } + + // 格式化为 YYYY-MM-DD + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + + return `${year}-${month}-${day}`; +}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..96ea903 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,25 @@ +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vite.dev/config/ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd()); + + return { + base: '/', + plugins: [react()], + build: { + outDir: 'dist', + }, + server: { + port: env.VITE_PORT, + proxy: { + '/api': { + target: env.VITE_API_URL || 'http://121.4.99.91:8008', + changeOrigin: true, + }, + }, + historyApiFallback: true, + }, + }; +});