Merge branch 'loginPage' into webui-node-expansion

This commit is contained in:
yangdx
2025-03-18 00:39:48 +08:00
24 changed files with 571 additions and 194 deletions

View File

@@ -341,7 +341,7 @@ def create_app(args):
ollama_api = OllamaAPI(rag, top_k=args.top_k)
app.include_router(ollama_api.router, prefix="/api")
@app.post("/login")
@app.post("/login", dependencies=[Depends(optional_api_key)])
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
username = os.getenv("AUTH_USERNAME")
password = os.getenv("AUTH_PASSWORD")

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,11 +5,11 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="icon" type="image/svg+xml" href="./logo.png" />
<link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title>
<script type="module" crossorigin src="./assets/index-DuxTk-ly.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-BEGlBF11.css">
<script type="module" crossorigin src="./assets/index-BP_n2eUy.js"></script>
<link rel="stylesheet" crossorigin href="./assets/index-DnTtpj6a.css">
</head>
<body>
<div id="root"></div>

View File

@@ -44,6 +44,7 @@
"react-i18next": "^15.4.1",
"react-markdown": "^9.1.0",
"react-number-format": "^5.4.3",
"react-router-dom": "^7.3.0",
"react-syntax-highlighter": "^15.6.1",
"rehype-react": "^8.0.0",
"remark-gfm": "^4.0.1",
@@ -419,6 +420,8 @@
"@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
"@types/cookie": ["@types/cookie@0.6.0", "https://registry.npmmirror.com/@types/cookie/-/cookie-0.6.0.tgz", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
@@ -567,6 +570,8 @@
"convert-source-map": ["convert-source-map@1.9.0", "", {}, "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="],
"cookie": ["cookie@1.0.2", "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="],
"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=="],
"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 @@
"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=="],
"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=="],
"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=="],
"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=="],
"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 @@
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"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=="],
"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=="],
"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 @@
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"turbo-stream": ["turbo-stream@2.4.0", "https://registry.npmmirror.com/turbo-stream/-/turbo-stream-2.4.0.tgz", {}, "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g=="],
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
"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=="],

View File

@@ -5,7 +5,7 @@
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
<link rel="icon" type="image/svg+xml" href="/logo.png" />
<link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title>
</head>

View File

@@ -53,6 +53,7 @@
"react-i18next": "^15.4.1",
"react-markdown": "^9.1.0",
"react-number-format": "^5.4.3",
"react-router-dom": "^7.3.0",
"react-syntax-highlighter": "^15.6.1",
"rehype-react": "^8.0.0",
"remark-gfm": "^4.0.1",

View File

@@ -8,7 +8,6 @@ import { healthCheckInterval } from '@/lib/constants'
import { useBackendState } from '@/stores/state'
import { useSettingsStore } from '@/stores/settings'
import { useEffect } from 'react'
import { Toaster } from 'sonner'
import SiteHeader from '@/features/SiteHeader'
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
@@ -81,7 +80,6 @@ function App() {
{enableHealthCheck && <StatusIndicator />}
{message !== null && !apiKeyInvalid && <MessageAlert />}
{apiKeyInvalid && <ApiKeyAlert />}
<Toaster />
</main>
</TabVisibilityProvider>
</ThemeProvider>

View File

@@ -0,0 +1,43 @@
import { HashRouter as Router, Routes, Route } from 'react-router-dom'
// import { useAuthStore } from '@/stores/state'
import { Toaster } from 'sonner'
import App from './App'
import LoginPage from '@/features/LoginPage'
import ThemeProvider from '@/components/ThemeProvider'
interface ProtectedRouteProps {
children: React.ReactNode
}
const ProtectedRoute = ({ children }: ProtectedRouteProps) => {
// const { isAuthenticated } = useAuthStore()
// if (!isAuthenticated) {
// return <Navigate to="/login" replace />
// }
return <>{children}</>
}
const AppRouter = () => {
return (
<ThemeProvider>
<Router>
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route
path="/*"
element={
<ProtectedRoute>
<App />
</ProtectedRoute>
}
/>
</Routes>
<Toaster position="top-center" />
</Router>
</ThemeProvider>
)
}
export default AppRouter

View File

@@ -1,7 +1,8 @@
import axios, { AxiosError } from 'axios'
import { backendBaseUrl } from '@/lib/constants'
import { backendBaseUrl, webuiPrefix } from '@/lib/constants'
import { errorMessage } from '@/lib/utils'
import { useSettingsStore } from '@/stores/settings'
import { useAuthStore } from '@/stores/state'
// Types
export type LightragNodeType = {
@@ -125,6 +126,11 @@ export type DocsStatusesResponse = {
statuses: Record<DocStatus, DocStatusResponse[]>
}
export type LoginResponse = {
access_token: string
token_type: string
}
export const InvalidApiKeyError = 'Invalid API Key'
export const RequireApiKeError = 'API Key required'
@@ -139,9 +145,13 @@ const axiosInstance = axios.create({
// Interceptoradd api key
axiosInstance.interceptors.request.use((config) => {
const apiKey = useSettingsStore.getState().apiKey
const token = localStorage.getItem('LIGHTRAG-API-TOKEN');
if (apiKey) {
config.headers['X-API-Key'] = apiKey
}
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
})
@@ -150,6 +160,21 @@ axiosInstance.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response) {
interface ErrorResponse {
detail: string;
}
if (error.response?.status === 401) {
localStorage.removeItem('LIGHTRAG-API-TOKEN');
sessionStorage.clear();
useAuthStore.getState().logout();
if (window.location.pathname !== `${webuiPrefix}/#/login`) {
window.location.href = `${webuiPrefix}/#/login`;
}
return Promise.reject(error);
}
throw new Error(
`${error.response.status} ${error.response.statusText}\n${JSON.stringify(
error.response.data
@@ -324,3 +349,17 @@ export const clearDocuments = async (): Promise<DocActionResponse> => {
const response = await axiosInstance.delete('/documents')
return response.data
}
export const loginToServer = async (username: string, password: string): Promise<LoginResponse> => {
const formData = new FormData();
formData.append('username', username);
formData.append('password', password);
const response = await axiosInstance.post('/login', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
return response.data;
}

View File

@@ -5,8 +5,13 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { useSettingsStore } from '@/stores/settings'
import { PaletteIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { cn } from '@/lib/utils'
export default function AppSettings() {
interface AppSettingsProps {
className?: string
}
export default function AppSettings({ className }: AppSettingsProps) {
const [opened, setOpened] = useState<boolean>(false)
const { t } = useTranslation()
@@ -27,7 +32,7 @@ export default function AppSettings() {
return (
<Popover open={opened} onOpenChange={setOpened}>
<PopoverTrigger asChild>
<Button variant="outline" size="icon" className="h-9 w-9">
<Button variant="ghost" size="icon" className={cn("h-9 w-9", className)}>
<PaletteIcon className="h-5 w-5" />
</Button>
</PopoverTrigger>

View 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>
)
}

View File

@@ -0,0 +1,106 @@
import { useState } from 'react'
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 AppSettings from '@/components/AppSettings'
const LoginPage = () => {
const navigate = useNavigate()
const { login } = useAuthStore()
const { t } = useTranslation()
const [loading, setLoading] = useState(false)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (!username || !password) {
toast.error(t('login.errorEmptyFields'))
return
}
try {
setLoading(true)
const response = await loginToServer(username, password)
login(response.access_token)
navigate('/')
toast.success(t('login.successMessage'))
} catch (error) {
console.error('Login failed...', error)
toast.error(t('login.errorInvalidCredentials'))
} finally {
setLoading(false)
}
}
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">
<AppSettings className="bg-white/30 dark:bg-gray-800/30 backdrop-blur-sm rounded-md" />
</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">
<div className="flex items-center gap-3">
<img src="logo.png" alt="LightRAG Logo" className="h-12 w-12" />
<ZapIcon className="size-10 text-emerald-400" aria-hidden="true" />
</div>
<div className="text-center space-y-2">
<h1 className="text-3xl font-bold tracking-tight">LightRAG</h1>
<p className="text-muted-foreground text-sm">
{t('login.description')}
</p>
</div>
</div>
</CardHeader>
<CardContent className="px-8 pb-8">
<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">
{t('login.username')}
</label>
<Input
id="username"
placeholder={t('login.usernamePlaceholder')}
value={username}
onChange={(e) => setUsername(e.target.value)}
required
className="h-11 flex-1"
/>
</div>
<div className="flex items-center gap-4">
<label htmlFor="password" className="text-sm font-medium w-16 shrink-0">
{t('login.password')}
</label>
<Input
id="password"
type="password"
placeholder={t('login.passwordPlaceholder')}
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="h-11 flex-1"
/>
</div>
<Button
type="submit"
className="w-full h-11 text-base font-medium mt-2"
disabled={loading}
>
{loading ? t('login.loggingIn') : t('login.loginButton')}
</Button>
</form>
</CardContent>
</Card>
</div>
)
}
export default LoginPage

View File

@@ -1,12 +1,14 @@
import Button from '@/components/ui/Button'
import { SiteInfo } from '@/lib/constants'
import { SiteInfo, webuiPrefix } from '@/lib/constants'
import AppSettings from '@/components/AppSettings'
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
import { useSettingsStore } from '@/stores/settings'
import { useAuthStore } from '@/stores/state'
import { cn } from '@/lib/utils'
import { useTranslation } from 'react-i18next'
import { useNavigate } from 'react-router-dom'
import { ZapIcon, GithubIcon } from 'lucide-react'
import { ZapIcon, GithubIcon, LogOutIcon } from 'lucide-react'
interface NavigationTabProps {
value: string
@@ -54,9 +56,17 @@ function TabsNavigation() {
export default function SiteHeader() {
const { t } = useTranslation()
const navigate = useNavigate()
const { logout } = useAuthStore()
const handleLogout = () => {
logout()
navigate('/login')
}
return (
<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">
<a href="/" className="mr-6 flex items-center gap-2">
<a href={webuiPrefix} className="mr-6 flex items-center gap-2">
<ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
{/* <img src='/logo.png' className="size-4" /> */}
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
@@ -74,6 +84,9 @@ export default function SiteHeader() {
</a>
</Button>
<AppSettings />
<Button variant="ghost" size="icon" side="bottom" tooltip={t('header.logout')} onClick={handleLogout}>
<LogOutIcon className="size-4" aria-hidden="true" />
</Button>
</div>
</nav>
</header>

View File

@@ -0,0 +1,35 @@
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({
resources: {
en: { translation: en },
zh: { translation: zh }
},
lng: getStoredLanguage(), // 使用存储的语言设置
fallbackLng: "en",
interpolation: {
escapeValue: false
}
});
export default i18n;

View File

@@ -1,6 +1,7 @@
import { ButtonVariantType } from '@/components/ui/Button'
export const backendBaseUrl = ''
export const webuiPrefix = ''
export const controlButtonVariant: ButtonVariantType = 'ghost'

View File

@@ -12,11 +12,24 @@
"retrieval": "Retrieval",
"api": "API",
"projectRepository": "Project Repository",
"logout": "Logout",
"themeToggle": {
"switchToLight": "Switch to light theme",
"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",

View File

@@ -12,11 +12,24 @@
"retrieval": "检索",
"api": "API",
"projectRepository": "项目仓库",
"logout": "退出登录",
"themeToggle": {
"switchToLight": "切换到浅色主题",
"switchToDark": "切换到深色主题"
}
},
"login": {
"description": "请输入您的账号和密码登录系统",
"username": "用户名",
"usernamePlaceholder": "请输入用户名",
"password": "密码",
"passwordPlaceholder": "请输入密码",
"loginButton": "登录",
"loggingIn": "登录中...",
"successMessage": "登录成功",
"errorEmptyFields": "请输入您的用户名和密码",
"errorInvalidCredentials": "登录失败,请检查用户名和密码"
},
"documentPanel": {
"clearDocuments": {
"button": "清空",

View File

@@ -1,5 +1,13 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import { Root } from '@/components/Root'
import AppRouter from './AppRouter'
import "./i18n";
createRoot(document.getElementById('root')!).render(<Root />)
createRoot(document.getElementById('root')!).render(
<StrictMode>
<AppRouter />
</StrictMode>
)

View File

@@ -7,6 +7,7 @@ import { Message, QueryRequest } from '@/api/lightrag'
type Theme = 'dark' | 'light' | 'system'
type Language = 'en' | 'zh'
type Tab = 'documents' | 'knowledge-graph' | 'retrieval' | 'api'
type Language = 'en' | 'zh'
interface SettingsState {
// Graph viewer settings

View File

@@ -16,6 +16,14 @@ interface BackendState {
setErrorMessage: (message: string, messageTitle: string) => void
}
interface AuthState {
isAuthenticated: boolean;
showLoginModal: boolean;
login: (token: string) => void;
logout: () => void;
setShowLoginModal: (show: boolean) => void;
}
const useBackendStateStoreBase = create<BackendState>()((set) => ({
health: true,
message: null,
@@ -57,3 +65,17 @@ const useBackendStateStoreBase = create<BackendState>()((set) => ({
const useBackendState = createSelectors(useBackendStateStoreBase)
export { useBackendState }
export const useAuthStore = create<AuthState>(set => ({
isAuthenticated: !!localStorage.getItem('LIGHTRAG-API-TOKEN'),
showLoginModal: false,
login: (token) => {
localStorage.setItem('LIGHTRAG-API-TOKEN', token);
set({ isAuthenticated: true, showLoginModal: false });
},
logout: () => {
localStorage.removeItem('LIGHTRAG-API-TOKEN');
set({ isAuthenticated: false });
},
setShowLoginModal: (show) => set({ showLoginModal: show })
}));

View File

@@ -26,5 +26,5 @@
"@/*": ["./src/*"]
}
},
"include": ["src", "vite.config.ts"]
"include": ["src", "vite.config.ts", "src/vite-env.d.ts"]
}

View File

@@ -1,6 +1,6 @@
import { defineConfig } from 'vite'
import path from 'path'
import { webuiPrefix } from '@/lib/constants'
import react from '@vitejs/plugin-react-swc'
import tailwindcss from '@tailwindcss/vite'
@@ -12,7 +12,8 @@ export default defineConfig({
'@': path.resolve(__dirname, './src')
}
},
base: './',
// base: import.meta.env.VITE_BASE_URL || '/webui/',
base: webuiPrefix,
build: {
outDir: path.resolve(__dirname, '../lightrag/api/webui'),
emptyOutDir: true