Added language and theme switching function to login page and homepage
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import ThemeProvider from '@/components/ThemeProvider'
|
||||
import MessageAlert from '@/components/MessageAlert'
|
||||
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
||||
import StatusIndicator from '@/components/graph/StatusIndicator'
|
||||
@@ -52,7 +51,6 @@ function App() {
|
||||
}, [message, setApiKeyInvalid])
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<main className="flex h-screen w-screen overflow-x-hidden">
|
||||
<Tabs
|
||||
defaultValue={currentTab}
|
||||
@@ -79,7 +77,6 @@ function App() {
|
||||
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
||||
{apiKeyInvalid && <ApiKeyAlert />}
|
||||
</main>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { Toaster } from 'sonner'
|
||||
import App from './App'
|
||||
import LoginPage from '@/features/LoginPage'
|
||||
import ThemeProvider from '@/components/ThemeProvider'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children: React.ReactNode
|
||||
@@ -20,20 +21,22 @@ const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
|
||||
|
||||
const AppRouter = () => {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<App />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Toaster position="top-center" />
|
||||
</BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<App />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
<Toaster position="top-center" />
|
||||
</BrowserRouter>
|
||||
</ThemeProvider>
|
||||
)
|
||||
}
|
||||
|
||||
|
49
lightrag_webui/src/components/LanguageToggle.tsx
Normal file
49
lightrag_webui/src/components/LanguageToggle.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import Button from '@/components/ui/Button'
|
||||
import { useCallback } from 'react'
|
||||
import { controlButtonVariant } from '@/lib/constants'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
|
||||
/**
|
||||
* Component that toggles the language between English and Chinese.
|
||||
*/
|
||||
export default function LanguageToggle() {
|
||||
const { i18n } = useTranslation()
|
||||
const currentLanguage = i18n.language
|
||||
const setLanguage = useSettingsStore.use.setLanguage()
|
||||
|
||||
const setEnglish = useCallback(() => {
|
||||
i18n.changeLanguage('en')
|
||||
setLanguage('en')
|
||||
}, [i18n, setLanguage])
|
||||
|
||||
const setChinese = useCallback(() => {
|
||||
i18n.changeLanguage('zh')
|
||||
setLanguage('zh')
|
||||
}, [i18n, setLanguage])
|
||||
|
||||
if (currentLanguage === 'zh') {
|
||||
return (
|
||||
<Button
|
||||
onClick={setEnglish}
|
||||
variant={controlButtonVariant}
|
||||
tooltip="Switch to English"
|
||||
size="icon"
|
||||
side="bottom"
|
||||
>
|
||||
中
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
onClick={setChinese}
|
||||
variant={controlButtonVariant}
|
||||
tooltip="切换到中文"
|
||||
size="icon"
|
||||
side="bottom"
|
||||
>
|
||||
EN
|
||||
</Button>
|
||||
)
|
||||
}
|
@@ -3,15 +3,19 @@ import { useNavigate } from 'react-router-dom'
|
||||
import { useAuthStore } from '@/stores/state'
|
||||
import { loginToServer } from '@/api/lightrag'
|
||||
import { toast } from 'sonner'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/Card'
|
||||
import Input from '@/components/ui/Input'
|
||||
import Button from '@/components/ui/Button'
|
||||
import { ZapIcon } from 'lucide-react'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import LanguageToggle from '@/components/LanguageToggle'
|
||||
|
||||
const LoginPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const { login } = useAuthStore()
|
||||
const { t } = useTranslation()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [username, setUsername] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
@@ -19,7 +23,7 @@ const LoginPage = () => {
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (!username || !password) {
|
||||
toast.error('Please enter your username and password')
|
||||
toast.error(t('login.errorEmptyFields'))
|
||||
return
|
||||
}
|
||||
|
||||
@@ -28,10 +32,10 @@ const LoginPage = () => {
|
||||
const response = await loginToServer(username, password)
|
||||
login(response.access_token)
|
||||
navigate('/')
|
||||
toast.success('Login succeeded')
|
||||
toast.success(t('login.successMessage'))
|
||||
} catch (error) {
|
||||
console.error('Login failed...', error)
|
||||
toast.error('Login failed, please check username and password')
|
||||
toast.error(t('login.errorInvalidCredentials'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -39,6 +43,10 @@ const LoginPage = () => {
|
||||
|
||||
return (
|
||||
<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">
|
||||
<div className="absolute top-4 right-4 flex items-center gap-2">
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
<Card className="w-full max-w-[480px] shadow-lg mx-4">
|
||||
<CardHeader className="flex items-center justify-center space-y-2 pb-8 pt-6">
|
||||
<div className="flex flex-col items-center space-y-4">
|
||||
@@ -49,7 +57,7 @@ const LoginPage = () => {
|
||||
<div className="text-center space-y-2">
|
||||
<h1 className="text-3xl font-bold tracking-tight">LightRAG</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Please enter your account and password to log in to the system
|
||||
{t('login.description')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -58,11 +66,11 @@ const LoginPage = () => {
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<label htmlFor="username" className="text-sm font-medium w-16 shrink-0">
|
||||
username
|
||||
{t('login.username')}
|
||||
</label>
|
||||
<Input
|
||||
id="username"
|
||||
placeholder="Please input a username"
|
||||
placeholder={t('login.usernamePlaceholder')}
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
required
|
||||
@@ -71,12 +79,12 @@ const LoginPage = () => {
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<label htmlFor="password" className="text-sm font-medium w-16 shrink-0">
|
||||
password
|
||||
{t('login.password')}
|
||||
</label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="Please input a password"
|
||||
placeholder={t('login.passwordPlaceholder')}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
@@ -88,7 +96,7 @@ const LoginPage = () => {
|
||||
className="w-full h-11 text-base font-medium mt-2"
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
{loading ? t('login.loggingIn') : t('login.loginButton')}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
import Button from '@/components/ui/Button'
|
||||
import { SiteInfo } from '@/lib/constants'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import LanguageToggle from '@/components/LanguageToggle'
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useAuthStore } from '@/stores/state'
|
||||
@@ -82,6 +83,7 @@ export default function SiteHeader() {
|
||||
<GithubIcon className="size-4" aria-hidden="true" />
|
||||
</a>
|
||||
</Button>
|
||||
<LanguageToggle />
|
||||
<ThemeToggle />
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
@@ -1,9 +1,23 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import { useSettingsStore } from "./stores/settings";
|
||||
|
||||
import en from "./locales/en.json";
|
||||
import zh from "./locales/zh.json";
|
||||
|
||||
const getStoredLanguage = () => {
|
||||
try {
|
||||
const settingsString = localStorage.getItem('settings-storage');
|
||||
if (settingsString) {
|
||||
const settings = JSON.parse(settingsString);
|
||||
return settings.state?.language || 'en';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to get stored language:', e);
|
||||
}
|
||||
return 'en';
|
||||
};
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
@@ -11,7 +25,7 @@ i18n
|
||||
en: { translation: en },
|
||||
zh: { translation: zh }
|
||||
},
|
||||
lng: "en", // default
|
||||
lng: getStoredLanguage(), // 使用存储的语言设置
|
||||
fallbackLng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
|
@@ -10,6 +10,18 @@
|
||||
"switchToDark": "Switch to dark theme"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"description": "Please enter your account and password to log in to the system",
|
||||
"username": "Username",
|
||||
"usernamePlaceholder": "Please input a username",
|
||||
"password": "Password",
|
||||
"passwordPlaceholder": "Please input a password",
|
||||
"loginButton": "Login",
|
||||
"loggingIn": "Logging in...",
|
||||
"successMessage": "Login succeeded",
|
||||
"errorEmptyFields": "Please enter your username and password",
|
||||
"errorInvalidCredentials": "Login failed, please check username and password"
|
||||
},
|
||||
"documentPanel": {
|
||||
"clearDocuments": {
|
||||
"button": "Clear",
|
||||
|
@@ -10,6 +10,18 @@
|
||||
"switchToDark": "切换到暗色主题"
|
||||
}
|
||||
},
|
||||
"login": {
|
||||
"description": "请输入您的账号和密码登录系统",
|
||||
"username": "用户名",
|
||||
"usernamePlaceholder": "请输入用户名",
|
||||
"password": "密码",
|
||||
"passwordPlaceholder": "请输入密码",
|
||||
"loginButton": "登录",
|
||||
"loggingIn": "登录中...",
|
||||
"successMessage": "登录成功",
|
||||
"errorEmptyFields": "请输入您的用户名和密码",
|
||||
"errorInvalidCredentials": "登录失败,请检查用户名和密码"
|
||||
},
|
||||
"documentPanel": {
|
||||
"clearDocuments": {
|
||||
"button": "清除",
|
||||
|
@@ -6,6 +6,7 @@ import { Message, QueryRequest } from '@/api/lightrag'
|
||||
|
||||
type Theme = 'dark' | 'light' | 'system'
|
||||
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
|
||||
type Language = 'en' | 'zh'
|
||||
|
||||
interface SettingsState {
|
||||
// Graph viewer settings
|
||||
@@ -46,6 +47,9 @@ interface SettingsState {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
|
||||
language: Language
|
||||
setLanguage: (language: Language) => void
|
||||
|
||||
enableHealthCheck: boolean
|
||||
setEnableHealthCheck: (enable: boolean) => void
|
||||
|
||||
@@ -57,6 +61,7 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
theme: 'system',
|
||||
language: 'en',
|
||||
|
||||
showPropertyPanel: true,
|
||||
showNodeSearchBar: true,
|
||||
@@ -99,6 +104,8 @@ const useSettingsStoreBase = create<SettingsState>()(
|
||||
|
||||
setTheme: (theme: Theme) => set({ theme }),
|
||||
|
||||
setLanguage: (language: Language) => set({ language }),
|
||||
|
||||
setGraphLayoutMaxIterations: (iterations: number) =>
|
||||
set({
|
||||
graphLayoutMaxIterations: iterations
|
||||
|
Reference in New Issue
Block a user