yangdx commited on
Commit
79819d7
·
2 Parent(s): 81f83c9 50ccf38

Merge branch 'loginPage' into webui-node-expansion

Browse files
lightrag/api/lightrag_server.py CHANGED
@@ -341,7 +341,7 @@ def create_app(args):
341
  ollama_api = OllamaAPI(rag, top_k=args.top_k)
342
  app.include_router(ollama_api.router, prefix="/api")
343
 
344
- @app.post("/login")
345
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
346
  username = os.getenv("AUTH_USERNAME")
347
  password = os.getenv("AUTH_PASSWORD")
 
341
  ollama_api = OllamaAPI(rag, top_k=args.top_k)
342
  app.include_router(ollama_api.router, prefix="/api")
343
 
344
+ @app.post("/login", dependencies=[Depends(optional_api_key)])
345
  async def login(form_data: OAuth2PasswordRequestForm = Depends()):
346
  username = os.getenv("AUTH_USERNAME")
347
  password = os.getenv("AUTH_PASSWORD")
lightrag/api/webui/assets/index-BEGlBF11.css DELETED
Binary file (49.1 kB)
 
lightrag/api/webui/assets/{index-DuxTk-ly.js → index-BP_n2eUy.js} RENAMED
Binary files a/lightrag/api/webui/assets/index-DuxTk-ly.js and b/lightrag/api/webui/assets/index-BP_n2eUy.js differ
 
lightrag/api/webui/assets/index-DnTtpj6a.css ADDED
Binary file (52.2 kB). View file
 
lightrag/api/webui/index.html CHANGED
Binary files a/lightrag/api/webui/index.html and b/lightrag/api/webui/index.html differ
 
lightrag_webui/bun.lock CHANGED
@@ -44,6 +44,7 @@
44
  "react-i18next": "^15.4.1",
45
  "react-markdown": "^9.1.0",
46
  "react-number-format": "^5.4.3",
 
47
  "react-syntax-highlighter": "^15.6.1",
48
  "rehype-react": "^8.0.0",
49
  "remark-gfm": "^4.0.1",
@@ -419,6 +420,8 @@
419
 
420
  "@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
421
 
 
 
422
  "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
423
 
424
  "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
@@ -567,6 +570,8 @@
567
 
568
  "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
569
 
 
 
570
  "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
571
 
572
  "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
@@ -1117,6 +1122,10 @@
1117
 
1118
  "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
1119
 
 
 
 
 
1120
  "react-select": ["react-select@5.10.0", "", { "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.8.1", "@floating-ui/dom": "^1.0.1", "@types/react-transition-group": "^4.4.0", "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", "use-isomorphic-layout-effect": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA=="],
1121
 
1122
  "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
@@ -1167,6 +1176,8 @@
1167
 
1168
  "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1169
 
 
 
1170
  "set-function-length": ["set-function-length@1.2.2", "", { "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" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
1171
 
1172
  "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
@@ -1237,6 +1248,8 @@
1237
 
1238
  "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1239
 
 
 
1240
  "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
1241
 
1242
  "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
 
44
  "react-i18next": "^15.4.1",
45
  "react-markdown": "^9.1.0",
46
  "react-number-format": "^5.4.3",
47
+ "react-router-dom": "^7.3.0",
48
  "react-syntax-highlighter": "^15.6.1",
49
  "rehype-react": "^8.0.0",
50
  "remark-gfm": "^4.0.1",
 
420
 
421
  "@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
422
 
423
+ "@types/cookie": ["@types/cookie@0.6.0", "https://registry.npmmirror.com/@types/cookie/-/cookie-0.6.0.tgz", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
424
+
425
  "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
426
 
427
  "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
 
570
 
571
  "convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
572
 
573
+ "cookie": ["cookie@1.0.2", "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
574
+
575
  "cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="],
576
 
577
  "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
 
1122
 
1123
  "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
1124
 
1125
+ "react-router": ["react-router@7.3.0", "https://registry.npmmirror.com/react-router/-/react-router-7.3.0.tgz", { "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0", "turbo-stream": "2.4.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-466f2W7HIWaNXTKM5nHTqNxLrHTyXybm7R0eBlVSt0k/u55tTCDO194OIx/NrYD4TS5SXKTNekXfT37kMKUjgw=="],
1126
+
1127
+ "react-router-dom": ["react-router-dom@7.3.0", "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.3.0.tgz", { "dependencies": { "react-router": "7.3.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" } }, "sha512-z7Q5FTiHGgQfEurX/FBinkOXhWREJIAB2RiU24lvcBa82PxUpwqvs/PAXb9lJyPjTs2jrl6UkLvCZVGJPeNuuQ=="],
1128
+
1129
  "react-select": ["react-select@5.10.0", "", { "dependencies": { "@babel/runtime": "^7.12.0", "@emotion/cache": "^11.4.0", "@emotion/react": "^11.8.1", "@floating-ui/dom": "^1.0.1", "@types/react-transition-group": "^4.4.0", "memoize-one": "^6.0.0", "prop-types": "^15.6.0", "react-transition-group": "^4.3.0", "use-isomorphic-layout-effect": "^1.2.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-k96gw+i6N3ExgDwPIg0lUPmexl1ygPe6u5BdQFNBhkpbwroIgCNXdubtIzHfThYXYYTubwOBafoMnn7ruEP1xA=="],
1130
 
1131
  "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
 
1176
 
1177
  "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
1178
 
1179
+ "set-cookie-parser": ["set-cookie-parser@2.7.1", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", {}, "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ=="],
1180
+
1181
  "set-function-length": ["set-function-length@1.2.2", "", { "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" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
1182
 
1183
  "set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
 
1248
 
1249
  "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
1250
 
1251
+ "turbo-stream": ["turbo-stream@2.4.0", "https://registry.npmmirror.com/turbo-stream/-/turbo-stream-2.4.0.tgz", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
1252
+
1253
  "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
1254
 
1255
  "typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="],
lightrag_webui/index.html CHANGED
@@ -5,7 +5,7 @@
5
  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
6
  <meta http-equiv="Pragma" content="no-cache" />
7
  <meta http-equiv="Expires" content="0" />
8
- <link rel="icon" type="image/svg+xml" href="/logo.png" />
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
  <title>Lightrag</title>
11
  </head>
 
5
  <meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
6
  <meta http-equiv="Pragma" content="no-cache" />
7
  <meta http-equiv="Expires" content="0" />
8
+ <link rel="icon" type="image/svg+xml" href="logo.png" />
9
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
10
  <title>Lightrag</title>
11
  </head>
lightrag_webui/package.json CHANGED
@@ -53,6 +53,7 @@
53
  "react-i18next": "^15.4.1",
54
  "react-markdown": "^9.1.0",
55
  "react-number-format": "^5.4.3",
 
56
  "react-syntax-highlighter": "^15.6.1",
57
  "rehype-react": "^8.0.0",
58
  "remark-gfm": "^4.0.1",
 
53
  "react-i18next": "^15.4.1",
54
  "react-markdown": "^9.1.0",
55
  "react-number-format": "^5.4.3",
56
+ "react-router-dom": "^7.3.0",
57
  "react-syntax-highlighter": "^15.6.1",
58
  "rehype-react": "^8.0.0",
59
  "remark-gfm": "^4.0.1",
lightrag_webui/src/App.tsx CHANGED
@@ -8,7 +8,6 @@ import { healthCheckInterval } from '@/lib/constants'
8
  import { useBackendState } from '@/stores/state'
9
  import { useSettingsStore } from '@/stores/settings'
10
  import { useEffect } from 'react'
11
- import { Toaster } from 'sonner'
12
  import SiteHeader from '@/features/SiteHeader'
13
  import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
14
 
@@ -81,7 +80,6 @@ function App() {
81
  {enableHealthCheck && <StatusIndicator />}
82
  {message !== null && !apiKeyInvalid && <MessageAlert />}
83
  {apiKeyInvalid && <ApiKeyAlert />}
84
- <Toaster />
85
  </main>
86
  </TabVisibilityProvider>
87
  </ThemeProvider>
 
8
  import { useBackendState } from '@/stores/state'
9
  import { useSettingsStore } from '@/stores/settings'
10
  import { useEffect } from 'react'
 
11
  import SiteHeader from '@/features/SiteHeader'
12
  import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
13
 
 
80
  {enableHealthCheck && <StatusIndicator />}
81
  {message !== null && !apiKeyInvalid && <MessageAlert />}
82
  {apiKeyInvalid && <ApiKeyAlert />}
 
83
  </main>
84
  </TabVisibilityProvider>
85
  </ThemeProvider>
lightrag_webui/src/AppRouter.tsx ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HashRouter as Router, Routes, Route } from 'react-router-dom'
2
+ // import { useAuthStore } from '@/stores/state'
3
+ import { Toaster } from 'sonner'
4
+ import App from './App'
5
+ import LoginPage from '@/features/LoginPage'
6
+ import ThemeProvider from '@/components/ThemeProvider'
7
+
8
+ interface ProtectedRouteProps {
9
+ children: React.ReactNode
10
+ }
11
+
12
+ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
13
+ // const { isAuthenticated } = useAuthStore()
14
+
15
+ // if (!isAuthenticated) {
16
+ // return <Navigate to="/login" replace />
17
+ // }
18
+
19
+ return <>{children}</>
20
+ }
21
+
22
+ const AppRouter = () => {
23
+ return (
24
+ <ThemeProvider>
25
+ <Router>
26
+ <Routes>
27
+ <Route path="/login" element={<LoginPage />} />
28
+ <Route
29
+ path="/*"
30
+ element={
31
+ <ProtectedRoute>
32
+ <App />
33
+ </ProtectedRoute>
34
+ }
35
+ />
36
+ </Routes>
37
+ <Toaster position="top-center" />
38
+ </Router>
39
+ </ThemeProvider>
40
+ )
41
+ }
42
+
43
+ export default AppRouter
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -1,7 +1,8 @@
1
  import axios, { AxiosError } from 'axios'
2
- import { backendBaseUrl } from '@/lib/constants'
3
  import { errorMessage } from '@/lib/utils'
4
  import { useSettingsStore } from '@/stores/settings'
 
5
 
6
  // Types
7
  export type LightragNodeType = {
@@ -125,6 +126,11 @@ export type DocsStatusesResponse = {
125
  statuses: Record<DocStatus, DocStatusResponse[]>
126
  }
127
 
 
 
 
 
 
128
  export const InvalidApiKeyError = 'Invalid API Key'
129
  export const RequireApiKeError = 'API Key required'
130
 
@@ -139,9 +145,13 @@ const axiosInstance = axios.create({
139
  // Interceptor:add api key
140
  axiosInstance.interceptors.request.use((config) => {
141
  const apiKey = useSettingsStore.getState().apiKey
 
142
  if (apiKey) {
143
  config.headers['X-API-Key'] = apiKey
144
  }
 
 
 
145
  return config
146
  })
147
 
@@ -150,6 +160,21 @@ axiosInstance.interceptors.response.use(
150
  (response) => response,
151
  (error: AxiosError) => {
152
  if (error.response) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  throw new Error(
154
  `${error.response.status} ${error.response.statusText}\n${JSON.stringify(
155
  error.response.data
@@ -324,3 +349,17 @@ export const clearDocuments = async (): Promise<DocActionResponse> => {
324
  const response = await axiosInstance.delete('/documents')
325
  return response.data
326
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import axios, { AxiosError } from 'axios'
2
+ import { backendBaseUrl, webuiPrefix } from '@/lib/constants'
3
  import { errorMessage } from '@/lib/utils'
4
  import { useSettingsStore } from '@/stores/settings'
5
+ import { useAuthStore } from '@/stores/state'
6
 
7
  // Types
8
  export type LightragNodeType = {
 
126
  statuses: Record<DocStatus, DocStatusResponse[]>
127
  }
128
 
129
+ export type LoginResponse = {
130
+ access_token: string
131
+ token_type: string
132
+ }
133
+
134
  export const InvalidApiKeyError = 'Invalid API Key'
135
  export const RequireApiKeError = 'API Key required'
136
 
 
145
  // Interceptor:add api key
146
  axiosInstance.interceptors.request.use((config) => {
147
  const apiKey = useSettingsStore.getState().apiKey
148
+ const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
149
  if (apiKey) {
150
  config.headers['X-API-Key'] = apiKey
151
  }
152
+ if (token) {
153
+ config.headers['Authorization'] = `Bearer ${token}`
154
+ }
155
  return config
156
  })
157
 
 
160
  (response) => response,
161
  (error: AxiosError) => {
162
  if (error.response) {
163
+ interface ErrorResponse {
164
+ detail: string;
165
+ }
166
+
167
+ if (error.response?.status === 401) {
168
+ localStorage.removeItem('LIGHTRAG-API-TOKEN');
169
+ sessionStorage.clear();
170
+ useAuthStore.getState().logout();
171
+
172
+ if (window.location.pathname !== `${webuiPrefix}/#/login`) {
173
+ window.location.href = `${webuiPrefix}/#/login`;
174
+ }
175
+
176
+ return Promise.reject(error);
177
+ }
178
  throw new Error(
179
  `${error.response.status} ${error.response.statusText}\n${JSON.stringify(
180
  error.response.data
 
349
  const response = await axiosInstance.delete('/documents')
350
  return response.data
351
  }
352
+
353
+ export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
354
+ const formData = new FormData();
355
+ formData.append('username', username);
356
+ formData.append('password', password);
357
+
358
+ const response = await axiosInstance.post('/login', formData, {
359
+ headers: {
360
+ 'Content-Type': 'multipart/form-data'
361
+ }
362
+ });
363
+
364
+ return response.data;
365
+ }
lightrag_webui/src/components/AppSettings.tsx CHANGED
@@ -5,8 +5,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
5
  import { useSettingsStore } from '@/stores/settings'
6
  import { PaletteIcon } from 'lucide-react'
7
  import { useTranslation } from 'react-i18next'
 
8
 
9
- export default function AppSettings() {
 
 
 
 
10
  const [opened, setOpened] = useState<boolean>(false)
11
  const { t } = useTranslation()
12
 
@@ -27,7 +32,7 @@ export default function AppSettings() {
27
  return (
28
  <Popover open={opened} onOpenChange={setOpened}>
29
  <PopoverTrigger asChild>
30
- <Button variant="outline" size="icon" className="h-9 w-9">
31
  <PaletteIcon className="h-5 w-5" />
32
  </Button>
33
  </PopoverTrigger>
 
5
  import { useSettingsStore } from '@/stores/settings'
6
  import { PaletteIcon } from 'lucide-react'
7
  import { useTranslation } from 'react-i18next'
8
+ import { cn } from '@/lib/utils'
9
 
10
+ interface AppSettingsProps {
11
+ className?: string
12
+ }
13
+
14
+ export default function AppSettings({ className }: AppSettingsProps) {
15
  const [opened, setOpened] = useState<boolean>(false)
16
  const { t } = useTranslation()
17
 
 
32
  return (
33
  <Popover open={opened} onOpenChange={setOpened}>
34
  <PopoverTrigger asChild>
35
+ <Button variant="ghost" size="icon" className={cn("h-9 w-9", className)}>
36
  <PaletteIcon className="h-5 w-5" />
37
  </Button>
38
  </PopoverTrigger>
lightrag_webui/src/components/LanguageToggle.tsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Button from '@/components/ui/Button'
2
+ import { useCallback } from 'react'
3
+ import { controlButtonVariant } from '@/lib/constants'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useSettingsStore } from '@/stores/settings'
6
+
7
+ /**
8
+ * Component that toggles the language between English and Chinese.
9
+ */
10
+ export default function LanguageToggle() {
11
+ const { i18n } = useTranslation()
12
+ const currentLanguage = i18n.language
13
+ const setLanguage = useSettingsStore.use.setLanguage()
14
+
15
+ const setEnglish = useCallback(() => {
16
+ i18n.changeLanguage('en')
17
+ setLanguage('en')
18
+ }, [i18n, setLanguage])
19
+
20
+ const setChinese = useCallback(() => {
21
+ i18n.changeLanguage('zh')
22
+ setLanguage('zh')
23
+ }, [i18n, setLanguage])
24
+
25
+ if (currentLanguage === 'zh') {
26
+ return (
27
+ <Button
28
+ onClick={setEnglish}
29
+ variant={controlButtonVariant}
30
+ tooltip="Switch to English"
31
+ size="icon"
32
+ side="bottom"
33
+ >
34
+
35
+ </Button>
36
+ )
37
+ }
38
+ return (
39
+ <Button
40
+ onClick={setChinese}
41
+ variant={controlButtonVariant}
42
+ tooltip="切换到中文"
43
+ size="icon"
44
+ side="bottom"
45
+ >
46
+ EN
47
+ </Button>
48
+ )
49
+ }
lightrag_webui/src/features/LoginPage.tsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { useAuthStore } from '@/stores/state'
4
+ import { loginToServer } from '@/api/lightrag'
5
+ import { toast } from 'sonner'
6
+ import { useTranslation } from 'react-i18next'
7
+
8
+ import { Card, CardContent, CardHeader } from '@/components/ui/Card'
9
+ import Input from '@/components/ui/Input'
10
+ import Button from '@/components/ui/Button'
11
+ import { ZapIcon } from 'lucide-react'
12
+ import AppSettings from '@/components/AppSettings'
13
+
14
+ const LoginPage = () => {
15
+ const navigate = useNavigate()
16
+ const { login } = useAuthStore()
17
+ const { t } = useTranslation()
18
+ const [loading, setLoading] = useState(false)
19
+ const [username, setUsername] = useState('')
20
+ const [password, setPassword] = useState('')
21
+
22
+ const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
23
+ e.preventDefault()
24
+ if (!username || !password) {
25
+ toast.error(t('login.errorEmptyFields'))
26
+ return
27
+ }
28
+
29
+ try {
30
+ setLoading(true)
31
+ const response = await loginToServer(username, password)
32
+ login(response.access_token)
33
+ navigate('/')
34
+ toast.success(t('login.successMessage'))
35
+ } catch (error) {
36
+ console.error('Login failed...', error)
37
+ toast.error(t('login.errorInvalidCredentials'))
38
+ } finally {
39
+ setLoading(false)
40
+ }
41
+ }
42
+
43
+ return (
44
+ <div className="flex h-screen w-screen items-center justify-center bg-gradient-to-br from-emerald-50 to-teal-100 dark:from-gray-900 dark:to-gray-800">
45
+ <div className="absolute top-4 right-4 flex items-center gap-2">
46
+ <AppSettings className="bg-white/30 dark:bg-gray-800/30 backdrop-blur-sm rounded-md" />
47
+ </div>
48
+ <Card className="w-full max-w-[480px] shadow-lg mx-4">
49
+ <CardHeader className="flex items-center justify-center space-y-2 pb-8 pt-6">
50
+ <div className="flex flex-col items-center space-y-4">
51
+ <div className="flex items-center gap-3">
52
+ <img src="logo.png" alt="LightRAG Logo" className="h-12 w-12" />
53
+ <ZapIcon className="size-10 text-emerald-400" aria-hidden="true" />
54
+ </div>
55
+ <div className="text-center space-y-2">
56
+ <h1 className="text-3xl font-bold tracking-tight">LightRAG</h1>
57
+ <p className="text-muted-foreground text-sm">
58
+ {t('login.description')}
59
+ </p>
60
+ </div>
61
+ </div>
62
+ </CardHeader>
63
+ <CardContent className="px-8 pb-8">
64
+ <form onSubmit={handleSubmit} className="space-y-6">
65
+ <div className="flex items-center gap-4">
66
+ <label htmlFor="username" className="text-sm font-medium w-16 shrink-0">
67
+ {t('login.username')}
68
+ </label>
69
+ <Input
70
+ id="username"
71
+ placeholder={t('login.usernamePlaceholder')}
72
+ value={username}
73
+ onChange={(e) => setUsername(e.target.value)}
74
+ required
75
+ className="h-11 flex-1"
76
+ />
77
+ </div>
78
+ <div className="flex items-center gap-4">
79
+ <label htmlFor="password" className="text-sm font-medium w-16 shrink-0">
80
+ {t('login.password')}
81
+ </label>
82
+ <Input
83
+ id="password"
84
+ type="password"
85
+ placeholder={t('login.passwordPlaceholder')}
86
+ value={password}
87
+ onChange={(e) => setPassword(e.target.value)}
88
+ required
89
+ className="h-11 flex-1"
90
+ />
91
+ </div>
92
+ <Button
93
+ type="submit"
94
+ className="w-full h-11 text-base font-medium mt-2"
95
+ disabled={loading}
96
+ >
97
+ {loading ? t('login.loggingIn') : t('login.loginButton')}
98
+ </Button>
99
+ </form>
100
+ </CardContent>
101
+ </Card>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ export default LoginPage
lightrag_webui/src/features/SiteHeader.tsx CHANGED
@@ -1,12 +1,14 @@
1
  import Button from '@/components/ui/Button'
2
- import { SiteInfo } from '@/lib/constants'
3
  import AppSettings from '@/components/AppSettings'
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
  import { useSettingsStore } from '@/stores/settings'
 
6
  import { cn } from '@/lib/utils'
7
  import { useTranslation } from 'react-i18next'
 
8
 
9
- import { ZapIcon, GithubIcon } from 'lucide-react'
10
 
11
  interface NavigationTabProps {
12
  value: string
@@ -54,9 +56,17 @@ function TabsNavigation() {
54
 
55
  export default function SiteHeader() {
56
  const { t } = useTranslation()
 
 
 
 
 
 
 
 
57
  return (
58
  <header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
59
- <a href="/" className="mr-6 flex items-center gap-2">
60
  <ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
61
  {/* <img src='/logo.png' className="size-4" /> */}
62
  <span className="font-bold md:inline-block">{SiteInfo.name}</span>
@@ -74,6 +84,9 @@ export default function SiteHeader() {
74
  </a>
75
  </Button>
76
  <AppSettings />
 
 
 
77
  </div>
78
  </nav>
79
  </header>
 
1
  import Button from '@/components/ui/Button'
2
+ import { SiteInfo, webuiPrefix } from '@/lib/constants'
3
  import AppSettings from '@/components/AppSettings'
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
  import { useSettingsStore } from '@/stores/settings'
6
+ import { useAuthStore } from '@/stores/state'
7
  import { cn } from '@/lib/utils'
8
  import { useTranslation } from 'react-i18next'
9
+ import { useNavigate } from 'react-router-dom'
10
 
11
+ import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
12
 
13
  interface NavigationTabProps {
14
  value: string
 
56
 
57
  export default function SiteHeader() {
58
  const { t } = useTranslation()
59
+ const navigate = useNavigate()
60
+ const { logout } = useAuthStore()
61
+
62
+ const handleLogout = () => {
63
+ logout()
64
+ navigate('/login')
65
+ }
66
+
67
  return (
68
  <header className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 flex h-10 w-full border-b px-4 backdrop-blur">
69
+ <a href={webuiPrefix} className="mr-6 flex items-center gap-2">
70
  <ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
71
  {/* <img src='/logo.png' className="size-4" /> */}
72
  <span className="font-bold md:inline-block">{SiteInfo.name}</span>
 
84
  </a>
85
  </Button>
86
  <AppSettings />
87
+ <Button variant="ghost" size="icon" side="bottom" tooltip={t('header.logout')} onClick={handleLogout}>
88
+ <LogOutIcon className="size-4" aria-hidden="true" />
89
+ </Button>
90
  </div>
91
  </nav>
92
  </header>
lightrag_webui/src/i18n.js ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import i18n from "i18next";
2
+ import { initReactI18next } from "react-i18next";
3
+ import { useSettingsStore } from "./stores/settings";
4
+
5
+ import en from "./locales/en.json";
6
+ import zh from "./locales/zh.json";
7
+
8
+ const getStoredLanguage = () => {
9
+ try {
10
+ const settingsString = localStorage.getItem('settings-storage');
11
+ if (settingsString) {
12
+ const settings = JSON.parse(settingsString);
13
+ return settings.state?.language || 'en';
14
+ }
15
+ } catch (e) {
16
+ console.error('Failed to get stored language:', e);
17
+ }
18
+ return 'en';
19
+ };
20
+
21
+ i18n
22
+ .use(initReactI18next)
23
+ .init({
24
+ resources: {
25
+ en: { translation: en },
26
+ zh: { translation: zh }
27
+ },
28
+ lng: getStoredLanguage(), // 使用存储的语言设置
29
+ fallbackLng: "en",
30
+ interpolation: {
31
+ escapeValue: false
32
+ }
33
+ });
34
+
35
+ export default i18n;
lightrag_webui/src/lib/constants.ts CHANGED
@@ -1,6 +1,7 @@
1
  import { ButtonVariantType } from '@/components/ui/Button'
2
 
3
  export const backendBaseUrl = ''
 
4
 
5
  export const controlButtonVariant: ButtonVariantType = 'ghost'
6
 
 
1
  import { ButtonVariantType } from '@/components/ui/Button'
2
 
3
  export const backendBaseUrl = ''
4
+ export const webuiPrefix = ''
5
 
6
  export const controlButtonVariant: ButtonVariantType = 'ghost'
7
 
lightrag_webui/src/locales/en.json CHANGED
@@ -12,11 +12,24 @@
12
  "retrieval": "Retrieval",
13
  "api": "API",
14
  "projectRepository": "Project Repository",
 
15
  "themeToggle": {
16
  "switchToLight": "Switch to light theme",
17
  "switchToDark": "Switch to dark theme"
18
  }
19
  },
 
 
 
 
 
 
 
 
 
 
 
 
20
  "documentPanel": {
21
  "clearDocuments": {
22
  "button": "Clear",
 
12
  "retrieval": "Retrieval",
13
  "api": "API",
14
  "projectRepository": "Project Repository",
15
+ "logout": "Logout",
16
  "themeToggle": {
17
  "switchToLight": "Switch to light theme",
18
  "switchToDark": "Switch to dark theme"
19
  }
20
  },
21
+ "login": {
22
+ "description": "Please enter your account and password to log in to the system",
23
+ "username": "Username",
24
+ "usernamePlaceholder": "Please input a username",
25
+ "password": "Password",
26
+ "passwordPlaceholder": "Please input a password",
27
+ "loginButton": "Login",
28
+ "loggingIn": "Logging in...",
29
+ "successMessage": "Login succeeded",
30
+ "errorEmptyFields": "Please enter your username and password",
31
+ "errorInvalidCredentials": "Login failed, please check username and password"
32
+ },
33
  "documentPanel": {
34
  "clearDocuments": {
35
  "button": "Clear",
lightrag_webui/src/locales/zh.json CHANGED
@@ -12,11 +12,24 @@
12
  "retrieval": "检索",
13
  "api": "API",
14
  "projectRepository": "项目仓库",
 
15
  "themeToggle": {
16
  "switchToLight": "切换到浅色主题",
17
  "switchToDark": "切换到深色主题"
18
  }
19
  },
 
 
 
 
 
 
 
 
 
 
 
 
20
  "documentPanel": {
21
  "clearDocuments": {
22
  "button": "清空",
 
12
  "retrieval": "检索",
13
  "api": "API",
14
  "projectRepository": "项目仓库",
15
+ "logout": "退出登录",
16
  "themeToggle": {
17
  "switchToLight": "切换到浅色主题",
18
  "switchToDark": "切换到深色主题"
19
  }
20
  },
21
+ "login": {
22
+ "description": "请输入您的账号和密码登录系统",
23
+ "username": "用户名",
24
+ "usernamePlaceholder": "请输入用户名",
25
+ "password": "密码",
26
+ "passwordPlaceholder": "请输入密码",
27
+ "loginButton": "登录",
28
+ "loggingIn": "登录中...",
29
+ "successMessage": "登录成功",
30
+ "errorEmptyFields": "请输入您的用户名和密码",
31
+ "errorInvalidCredentials": "登录失败,请检查用户名和密码"
32
+ },
33
  "documentPanel": {
34
  "clearDocuments": {
35
  "button": "清空",
lightrag_webui/src/main.tsx CHANGED
@@ -1,5 +1,13 @@
 
1
  import { createRoot } from 'react-dom/client'
2
  import './index.css'
3
- import { Root } from '@/components/Root'
 
4
 
5
- createRoot(document.getElementById('root')!).render(<Root />)
 
 
 
 
 
 
 
1
+ import { StrictMode } from 'react'
2
  import { createRoot } from 'react-dom/client'
3
  import './index.css'
4
+ import AppRouter from './AppRouter'
5
+ import "./i18n";
6
 
7
+
8
+
9
+ createRoot(document.getElementById('root')!).render(
10
+ <StrictMode>
11
+ <AppRouter />
12
+ </StrictMode>
13
+ )
lightrag_webui/src/stores/settings.ts CHANGED
@@ -7,6 +7,7 @@ import { Message, QueryRequest } from '@/api/lightrag'
7
  type Theme = 'dark' | 'light' | 'system'
8
  type Language = 'en' | 'zh'
9
  type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
 
10
 
11
  interface SettingsState {
12
  // Graph viewer settings
 
7
  type Theme = 'dark' | 'light' | 'system'
8
  type Language = 'en' | 'zh'
9
  type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
10
+ type Language = 'en' | 'zh'
11
 
12
  interface SettingsState {
13
  // Graph viewer settings
lightrag_webui/src/stores/state.ts CHANGED
@@ -16,6 +16,14 @@ interface BackendState {
16
  setErrorMessage: (message: string, messageTitle: string) => void
17
  }
18
 
 
 
 
 
 
 
 
 
19
  const useBackendStateStoreBase = create<BackendState>()((set) => ({
20
  health: true,
21
  message: null,
@@ -57,3 +65,17 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
57
  const useBackendState = createSelectors(useBackendStateStoreBase)
58
 
59
  export { useBackendState }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  setErrorMessage: (message: string, messageTitle: string) => void
17
  }
18
 
19
+ interface AuthState {
20
+ isAuthenticated: boolean;
21
+ showLoginModal: boolean;
22
+ login: (token: string) => void;
23
+ logout: () => void;
24
+ setShowLoginModal: (show: boolean) => void;
25
+ }
26
+
27
  const useBackendStateStoreBase = create<BackendState>()((set) => ({
28
  health: true,
29
  message: null,
 
65
  const useBackendState = createSelectors(useBackendStateStoreBase)
66
 
67
  export { useBackendState }
68
+
69
+ export const useAuthStore = create<AuthState>(set => ({
70
+ isAuthenticated: !!localStorage.getItem('LIGHTRAG-API-TOKEN'),
71
+ showLoginModal: false,
72
+ login: (token) => {
73
+ localStorage.setItem('LIGHTRAG-API-TOKEN', token);
74
+ set({ isAuthenticated: true, showLoginModal: false });
75
+ },
76
+ logout: () => {
77
+ localStorage.removeItem('LIGHTRAG-API-TOKEN');
78
+ set({ isAuthenticated: false });
79
+ },
80
+ setShowLoginModal: (show) => set({ showLoginModal: show })
81
+ }));
lightrag_webui/tsconfig.json CHANGED
@@ -26,5 +26,5 @@
26
  "@/*": ["./src/*"]
27
  }
28
  },
29
- "include": ["src", "vite.config.ts"]
30
  }
 
26
  "@/*": ["./src/*"]
27
  }
28
  },
29
+ "include": ["src", "vite.config.ts", "src/vite-env.d.ts"]
30
  }
lightrag_webui/vite.config.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { defineConfig } from 'vite'
2
  import path from 'path'
3
-
4
  import react from '@vitejs/plugin-react-swc'
5
  import tailwindcss from '@tailwindcss/vite'
6
 
@@ -12,7 +12,8 @@ export default defineConfig({
12
  '@': path.resolve(__dirname, './src')
13
  }
14
  },
15
- base: './',
 
16
  build: {
17
  outDir: path.resolve(__dirname, '../lightrag/api/webui'),
18
  emptyOutDir: true
 
1
  import { defineConfig } from 'vite'
2
  import path from 'path'
3
+ import { webuiPrefix } from '@/lib/constants'
4
  import react from '@vitejs/plugin-react-swc'
5
  import tailwindcss from '@tailwindcss/vite'
6
 
 
12
  '@': path.resolve(__dirname, './src')
13
  }
14
  },
15
+ // base: import.meta.env.VITE_BASE_URL || '/webui/',
16
+ base: webuiPrefix,
17
  build: {
18
  outDir: path.resolve(__dirname, '../lightrag/api/webui'),
19
  emptyOutDir: true