choizhang
commited on
Commit
·
4421a58
1
Parent(s):
0a4b242
Added language and theme switching function to login page and homepage
Browse files- lightrag_webui/src/App.tsx +0 -3
- lightrag_webui/src/AppRouter.tsx +17 -14
- lightrag_webui/src/components/LanguageToggle.tsx +49 -0
- lightrag_webui/src/features/LoginPage.tsx +17 -9
- lightrag_webui/src/features/SiteHeader.tsx +2 -0
- lightrag_webui/src/i18n.js +15 -1
- lightrag_webui/src/locales/en.json +12 -0
- lightrag_webui/src/locales/zh.json +12 -0
- lightrag_webui/src/stores/settings.ts +7 -0
lightrag_webui/src/App.tsx
CHANGED
@@ -1,5 +1,4 @@
|
|
1 |
import { useState, useCallback } from 'react'
|
2 |
-
import ThemeProvider from '@/components/ThemeProvider'
|
3 |
import MessageAlert from '@/components/MessageAlert'
|
4 |
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
5 |
import StatusIndicator from '@/components/graph/StatusIndicator'
|
@@ -52,7 +51,6 @@ function App() {
|
|
52 |
}, [message, setApiKeyInvalid])
|
53 |
|
54 |
return (
|
55 |
-
<ThemeProvider>
|
56 |
<main className="flex h-screen w-screen overflow-x-hidden">
|
57 |
<Tabs
|
58 |
defaultValue={currentTab}
|
@@ -79,7 +77,6 @@ function App() {
|
|
79 |
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
80 |
{apiKeyInvalid && <ApiKeyAlert />}
|
81 |
</main>
|
82 |
-
</ThemeProvider>
|
83 |
)
|
84 |
}
|
85 |
|
|
|
1 |
import { useState, useCallback } from 'react'
|
|
|
2 |
import MessageAlert from '@/components/MessageAlert'
|
3 |
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
4 |
import StatusIndicator from '@/components/graph/StatusIndicator'
|
|
|
51 |
}, [message, setApiKeyInvalid])
|
52 |
|
53 |
return (
|
|
|
54 |
<main className="flex h-screen w-screen overflow-x-hidden">
|
55 |
<Tabs
|
56 |
defaultValue={currentTab}
|
|
|
77 |
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
78 |
{apiKeyInvalid && <ApiKeyAlert />}
|
79 |
</main>
|
|
|
80 |
)
|
81 |
}
|
82 |
|
lightrag_webui/src/AppRouter.tsx
CHANGED
@@ -3,6 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
|
3 |
import { Toaster } from 'sonner'
|
4 |
import App from './App'
|
5 |
import LoginPage from '@/features/LoginPage'
|
|
|
6 |
|
7 |
interface ProtectedRouteProps {
|
8 |
children: React.ReactNode
|
@@ -20,20 +21,22 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
|
20 |
|
21 |
const AppRouter = () => {
|
22 |
return (
|
23 |
-
<
|
24 |
-
<
|
25 |
-
<
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
|
30 |
-
<
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
|
|
|
|
37 |
)
|
38 |
}
|
39 |
|
|
|
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
|
|
|
21 |
|
22 |
const AppRouter = () => {
|
23 |
return (
|
24 |
+
<ThemeProvider>
|
25 |
+
<BrowserRouter>
|
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 |
+
</BrowserRouter>
|
39 |
+
</ThemeProvider>
|
40 |
)
|
41 |
}
|
42 |
|
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
CHANGED
@@ -3,15 +3,19 @@ import { useNavigate } from 'react-router-dom'
|
|
3 |
import { useAuthStore } from '@/stores/state'
|
4 |
import { loginToServer } from '@/api/lightrag'
|
5 |
import { toast } from 'sonner'
|
|
|
6 |
|
7 |
import { Card, CardContent, CardHeader } from '@/components/ui/Card'
|
8 |
import Input from '@/components/ui/Input'
|
9 |
import Button from '@/components/ui/Button'
|
10 |
import { ZapIcon } from 'lucide-react'
|
|
|
|
|
11 |
|
12 |
const LoginPage = () => {
|
13 |
const navigate = useNavigate()
|
14 |
const { login } = useAuthStore()
|
|
|
15 |
const [loading, setLoading] = useState(false)
|
16 |
const [username, setUsername] = useState('')
|
17 |
const [password, setPassword] = useState('')
|
@@ -19,7 +23,7 @@ const LoginPage = () => {
|
|
19 |
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
20 |
e.preventDefault()
|
21 |
if (!username || !password) {
|
22 |
-
toast.error('
|
23 |
return
|
24 |
}
|
25 |
|
@@ -28,10 +32,10 @@ const LoginPage = () => {
|
|
28 |
const response = await loginToServer(username, password)
|
29 |
login(response.access_token)
|
30 |
navigate('/')
|
31 |
-
toast.success('
|
32 |
} catch (error) {
|
33 |
console.error('Login failed...', error)
|
34 |
-
toast.error('
|
35 |
} finally {
|
36 |
setLoading(false)
|
37 |
}
|
@@ -39,6 +43,10 @@ const LoginPage = () => {
|
|
39 |
|
40 |
return (
|
41 |
<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">
|
|
|
|
|
|
|
|
|
42 |
<Card className="w-full max-w-[480px] shadow-lg mx-4">
|
43 |
<CardHeader className="flex items-center justify-center space-y-2 pb-8 pt-6">
|
44 |
<div className="flex flex-col items-center space-y-4">
|
@@ -49,7 +57,7 @@ const LoginPage = () => {
|
|
49 |
<div className="text-center space-y-2">
|
50 |
<h1 className="text-3xl font-bold tracking-tight">LightRAG</h1>
|
51 |
<p className="text-muted-foreground text-sm">
|
52 |
-
|
53 |
</p>
|
54 |
</div>
|
55 |
</div>
|
@@ -58,11 +66,11 @@ const LoginPage = () => {
|
|
58 |
<form onSubmit={handleSubmit} className="space-y-6">
|
59 |
<div className="flex items-center gap-4">
|
60 |
<label htmlFor="username" className="text-sm font-medium w-16 shrink-0">
|
61 |
-
|
62 |
</label>
|
63 |
<Input
|
64 |
id="username"
|
65 |
-
placeholder=
|
66 |
value={username}
|
67 |
onChange={(e) => setUsername(e.target.value)}
|
68 |
required
|
@@ -71,12 +79,12 @@ const LoginPage = () => {
|
|
71 |
</div>
|
72 |
<div className="flex items-center gap-4">
|
73 |
<label htmlFor="password" className="text-sm font-medium w-16 shrink-0">
|
74 |
-
|
75 |
</label>
|
76 |
<Input
|
77 |
id="password"
|
78 |
type="password"
|
79 |
-
placeholder=
|
80 |
value={password}
|
81 |
onChange={(e) => setPassword(e.target.value)}
|
82 |
required
|
@@ -88,7 +96,7 @@ const LoginPage = () => {
|
|
88 |
className="w-full h-11 text-base font-medium mt-2"
|
89 |
disabled={loading}
|
90 |
>
|
91 |
-
{loading ? '
|
92 |
</Button>
|
93 |
</form>
|
94 |
</CardContent>
|
|
|
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 ThemeToggle from '@/components/ThemeToggle'
|
13 |
+
import LanguageToggle from '@/components/LanguageToggle'
|
14 |
|
15 |
const LoginPage = () => {
|
16 |
const navigate = useNavigate()
|
17 |
const { login } = useAuthStore()
|
18 |
+
const { t } = useTranslation()
|
19 |
const [loading, setLoading] = useState(false)
|
20 |
const [username, setUsername] = useState('')
|
21 |
const [password, setPassword] = useState('')
|
|
|
23 |
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
24 |
e.preventDefault()
|
25 |
if (!username || !password) {
|
26 |
+
toast.error(t('login.errorEmptyFields'))
|
27 |
return
|
28 |
}
|
29 |
|
|
|
32 |
const response = await loginToServer(username, password)
|
33 |
login(response.access_token)
|
34 |
navigate('/')
|
35 |
+
toast.success(t('login.successMessage'))
|
36 |
} catch (error) {
|
37 |
console.error('Login failed...', error)
|
38 |
+
toast.error(t('login.errorInvalidCredentials'))
|
39 |
} finally {
|
40 |
setLoading(false)
|
41 |
}
|
|
|
43 |
|
44 |
return (
|
45 |
<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">
|
46 |
+
<div className="absolute top-4 right-4 flex items-center gap-2">
|
47 |
+
<LanguageToggle />
|
48 |
+
<ThemeToggle />
|
49 |
+
</div>
|
50 |
<Card className="w-full max-w-[480px] shadow-lg mx-4">
|
51 |
<CardHeader className="flex items-center justify-center space-y-2 pb-8 pt-6">
|
52 |
<div className="flex flex-col items-center space-y-4">
|
|
|
57 |
<div className="text-center space-y-2">
|
58 |
<h1 className="text-3xl font-bold tracking-tight">LightRAG</h1>
|
59 |
<p className="text-muted-foreground text-sm">
|
60 |
+
{t('login.description')}
|
61 |
</p>
|
62 |
</div>
|
63 |
</div>
|
|
|
66 |
<form onSubmit={handleSubmit} className="space-y-6">
|
67 |
<div className="flex items-center gap-4">
|
68 |
<label htmlFor="username" className="text-sm font-medium w-16 shrink-0">
|
69 |
+
{t('login.username')}
|
70 |
</label>
|
71 |
<Input
|
72 |
id="username"
|
73 |
+
placeholder={t('login.usernamePlaceholder')}
|
74 |
value={username}
|
75 |
onChange={(e) => setUsername(e.target.value)}
|
76 |
required
|
|
|
79 |
</div>
|
80 |
<div className="flex items-center gap-4">
|
81 |
<label htmlFor="password" className="text-sm font-medium w-16 shrink-0">
|
82 |
+
{t('login.password')}
|
83 |
</label>
|
84 |
<Input
|
85 |
id="password"
|
86 |
type="password"
|
87 |
+
placeholder={t('login.passwordPlaceholder')}
|
88 |
value={password}
|
89 |
onChange={(e) => setPassword(e.target.value)}
|
90 |
required
|
|
|
96 |
className="w-full h-11 text-base font-medium mt-2"
|
97 |
disabled={loading}
|
98 |
>
|
99 |
+
{loading ? t('login.loggingIn') : t('login.loginButton')}
|
100 |
</Button>
|
101 |
</form>
|
102 |
</CardContent>
|
lightrag_webui/src/features/SiteHeader.tsx
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
import Button from '@/components/ui/Button'
|
2 |
import { SiteInfo } from '@/lib/constants'
|
3 |
import ThemeToggle from '@/components/ThemeToggle'
|
|
|
4 |
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
5 |
import { useSettingsStore } from '@/stores/settings'
|
6 |
import { useAuthStore } from '@/stores/state'
|
@@ -82,6 +83,7 @@ export default function SiteHeader() {
|
|
82 |
<GithubIcon className="size-4" aria-hidden="true" />
|
83 |
</a>
|
84 |
</Button>
|
|
|
85 |
<ThemeToggle />
|
86 |
<Button
|
87 |
variant="ghost"
|
|
|
1 |
import Button from '@/components/ui/Button'
|
2 |
import { SiteInfo } from '@/lib/constants'
|
3 |
import ThemeToggle from '@/components/ThemeToggle'
|
4 |
+
import LanguageToggle from '@/components/LanguageToggle'
|
5 |
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
6 |
import { useSettingsStore } from '@/stores/settings'
|
7 |
import { useAuthStore } from '@/stores/state'
|
|
|
83 |
<GithubIcon className="size-4" aria-hidden="true" />
|
84 |
</a>
|
85 |
</Button>
|
86 |
+
<LanguageToggle />
|
87 |
<ThemeToggle />
|
88 |
<Button
|
89 |
variant="ghost"
|
lightrag_webui/src/i18n.js
CHANGED
@@ -1,9 +1,23 @@
|
|
1 |
import i18n from "i18next";
|
2 |
import { initReactI18next } from "react-i18next";
|
|
|
3 |
|
4 |
import en from "./locales/en.json";
|
5 |
import zh from "./locales/zh.json";
|
6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
i18n
|
8 |
.use(initReactI18next)
|
9 |
.init({
|
@@ -11,7 +25,7 @@ i18n
|
|
11 |
en: { translation: en },
|
12 |
zh: { translation: zh }
|
13 |
},
|
14 |
-
lng:
|
15 |
fallbackLng: "en",
|
16 |
interpolation: {
|
17 |
escapeValue: false
|
|
|
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({
|
|
|
25 |
en: { translation: en },
|
26 |
zh: { translation: zh }
|
27 |
},
|
28 |
+
lng: getStoredLanguage(), // 使用存储的语言设置
|
29 |
fallbackLng: "en",
|
30 |
interpolation: {
|
31 |
escapeValue: false
|
lightrag_webui/src/locales/en.json
CHANGED
@@ -10,6 +10,18 @@
|
|
10 |
"switchToDark": "Switch to dark theme"
|
11 |
}
|
12 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
"documentPanel": {
|
14 |
"clearDocuments": {
|
15 |
"button": "Clear",
|
|
|
10 |
"switchToDark": "Switch to dark theme"
|
11 |
}
|
12 |
},
|
13 |
+
"login": {
|
14 |
+
"description": "Please enter your account and password to log in to the system",
|
15 |
+
"username": "Username",
|
16 |
+
"usernamePlaceholder": "Please input a username",
|
17 |
+
"password": "Password",
|
18 |
+
"passwordPlaceholder": "Please input a password",
|
19 |
+
"loginButton": "Login",
|
20 |
+
"loggingIn": "Logging in...",
|
21 |
+
"successMessage": "Login succeeded",
|
22 |
+
"errorEmptyFields": "Please enter your username and password",
|
23 |
+
"errorInvalidCredentials": "Login failed, please check username and password"
|
24 |
+
},
|
25 |
"documentPanel": {
|
26 |
"clearDocuments": {
|
27 |
"button": "Clear",
|
lightrag_webui/src/locales/zh.json
CHANGED
@@ -10,6 +10,18 @@
|
|
10 |
"switchToDark": "切换到暗色主题"
|
11 |
}
|
12 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
13 |
"documentPanel": {
|
14 |
"clearDocuments": {
|
15 |
"button": "清除",
|
|
|
10 |
"switchToDark": "切换到暗色主题"
|
11 |
}
|
12 |
},
|
13 |
+
"login": {
|
14 |
+
"description": "请输入您的账号和密码登录系统",
|
15 |
+
"username": "用户名",
|
16 |
+
"usernamePlaceholder": "请输入用户名",
|
17 |
+
"password": "密码",
|
18 |
+
"passwordPlaceholder": "请输入密码",
|
19 |
+
"loginButton": "登录",
|
20 |
+
"loggingIn": "登录中...",
|
21 |
+
"successMessage": "登录成功",
|
22 |
+
"errorEmptyFields": "请输入您的用户名和密码",
|
23 |
+
"errorInvalidCredentials": "登录失败,请检查用户名和密码"
|
24 |
+
},
|
25 |
"documentPanel": {
|
26 |
"clearDocuments": {
|
27 |
"button": "清除",
|
lightrag_webui/src/stores/settings.ts
CHANGED
@@ -6,6 +6,7 @@ import { Message, QueryRequest } from '@/api/lightrag'
|
|
6 |
|
7 |
type Theme = 'dark' | 'light' | 'system'
|
8 |
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
|
|
9 |
|
10 |
interface SettingsState {
|
11 |
// Graph viewer settings
|
@@ -46,6 +47,9 @@ interface SettingsState {
|
|
46 |
theme: Theme
|
47 |
setTheme: (theme: Theme) => void
|
48 |
|
|
|
|
|
|
|
49 |
enableHealthCheck: boolean
|
50 |
setEnableHealthCheck: (enable: boolean) => void
|
51 |
|
@@ -57,6 +61,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
57 |
persist(
|
58 |
(set) => ({
|
59 |
theme: 'system',
|
|
|
60 |
|
61 |
showPropertyPanel: true,
|
62 |
showNodeSearchBar: true,
|
@@ -99,6 +104,8 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|
99 |
|
100 |
setTheme: (theme: Theme) => set({ theme }),
|
101 |
|
|
|
|
|
102 |
setGraphLayoutMaxIterations: (iterations: number) =>
|
103 |
set({
|
104 |
graphLayoutMaxIterations: iterations
|
|
|
6 |
|
7 |
type Theme = 'dark' | 'light' | 'system'
|
8 |
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
9 |
+
type Language = 'en' | 'zh'
|
10 |
|
11 |
interface SettingsState {
|
12 |
// Graph viewer settings
|
|
|
47 |
theme: Theme
|
48 |
setTheme: (theme: Theme) => void
|
49 |
|
50 |
+
language: Language
|
51 |
+
setLanguage: (language: Language) => void
|
52 |
+
|
53 |
enableHealthCheck: boolean
|
54 |
setEnableHealthCheck: (enable: boolean) => void
|
55 |
|
|
|
61 |
persist(
|
62 |
(set) => ({
|
63 |
theme: 'system',
|
64 |
+
language: 'en',
|
65 |
|
66 |
showPropertyPanel: true,
|
67 |
showNodeSearchBar: true,
|
|
|
104 |
|
105 |
setTheme: (theme: Theme) => set({ theme }),
|
106 |
|
107 |
+
setLanguage: (language: Language) => set({ language }),
|
108 |
+
|
109 |
setGraphLayoutMaxIterations: (iterations: number) =>
|
110 |
set({
|
111 |
graphLayoutMaxIterations: iterations
|