implement API key alert
This commit is contained in:
@@ -21,7 +21,7 @@ LightRAG WebUI is a React-based web interface for interacting with the LightRAG
|
||||
Run the following command to build the project:
|
||||
|
||||
```bash
|
||||
bun run build
|
||||
bun run build --emptyOutDir
|
||||
```
|
||||
|
||||
This command will bundle the project and output the built files to the `lightrag/api/webui` directory.
|
||||
|
@@ -5,6 +5,7 @@
|
||||
"name": "lightrag-webui",
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
@@ -220,6 +221,8 @@
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.1", "", {}, "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA=="],
|
||||
|
||||
"@radix-ui/react-alert-dialog": ["@radix-ui/react-alert-dialog@1.1.6", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-dialog": "1.1.6", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ=="],
|
||||
|
||||
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg=="],
|
||||
|
||||
"@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.1.4", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-presence": "1.1.2", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-previous": "1.1.0", "@radix-ui/react-use-size": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw=="],
|
||||
|
@@ -11,6 +11,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@faker-js/faker": "^9.5.0",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "^1.1.6",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
|
@@ -1,13 +1,15 @@
|
||||
import { useState, useCallback } from 'react'
|
||||
import ThemeProvider from '@/components/ThemeProvider'
|
||||
import ThemeProvider from '@/components/graph/ThemeProvider'
|
||||
import MessageAlert from '@/components/MessageAlert'
|
||||
import StatusIndicator from '@/components/StatusIndicator'
|
||||
import ApiKeyAlert from '@/components/ApiKeyAlert'
|
||||
import StatusIndicator from '@/components/graph/StatusIndicator'
|
||||
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'
|
||||
|
||||
import GraphViewer from '@/features/GraphViewer'
|
||||
import DocumentManager from '@/features/DocumentManager'
|
||||
@@ -20,6 +22,7 @@ function App() {
|
||||
const message = useBackendState.use.message()
|
||||
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||
const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
|
||||
const [apiKeyInvalid, setApiKeyInvalid] = useState(false)
|
||||
|
||||
// Health check
|
||||
useEffect(() => {
|
||||
@@ -39,6 +42,14 @@ function App() {
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) {
|
||||
setApiKeyInvalid(true)
|
||||
}
|
||||
}
|
||||
}, [message, setApiKeyInvalid])
|
||||
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<main className="flex h-screen w-screen overflow-x-hidden">
|
||||
@@ -64,7 +75,8 @@ function App() {
|
||||
</div>
|
||||
</Tabs>
|
||||
{enableHealthCheck && <StatusIndicator />}
|
||||
{message !== null && <MessageAlert />}
|
||||
{message !== null && !apiKeyInvalid && <MessageAlert />}
|
||||
{apiKeyInvalid && <ApiKeyAlert />}
|
||||
<Toaster />
|
||||
</main>
|
||||
</ThemeProvider>
|
||||
|
79
lightrag_webui/src/components/ApiKeyAlert.tsx
Normal file
79
lightrag_webui/src/components/ApiKeyAlert.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger
|
||||
} from '@/components/ui/AlertDialog'
|
||||
import Button from '@/components/ui/Button'
|
||||
import Input from '@/components/ui/Input'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
import { InvalidApiKeyError, RequireApiKeError } from '@/api/lightrag'
|
||||
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const ApiKeyAlert = () => {
|
||||
const [opened, setOpened] = useState<boolean>(true)
|
||||
const apiKey = useSettingsStore.use.apiKey()
|
||||
const [tempApiKey, setTempApiKey] = useState<string>('')
|
||||
const message = useBackendState.use.message()
|
||||
|
||||
useEffect(() => {
|
||||
setTempApiKey(apiKey || '')
|
||||
}, [apiKey, opened])
|
||||
|
||||
useEffect(() => {
|
||||
if (message) {
|
||||
if (message.includes(InvalidApiKeyError) || message.includes(RequireApiKeError)) {
|
||||
setOpened(true)
|
||||
}
|
||||
}
|
||||
}, [message, setOpened])
|
||||
|
||||
const setApiKey = useCallback(async () => {
|
||||
useSettingsStore.setState({ apiKey: tempApiKey || null })
|
||||
if (await useBackendState.getState().check()) {
|
||||
setOpened(false)
|
||||
return
|
||||
}
|
||||
toast.error('API Key is invalid')
|
||||
}, [tempApiKey])
|
||||
|
||||
const handleTempApiKeyChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setTempApiKey(e.target.value)
|
||||
},
|
||||
[setTempApiKey]
|
||||
)
|
||||
|
||||
return (
|
||||
<AlertDialog open={opened} onOpenChange={setOpened}>
|
||||
<AlertDialogTrigger>Open</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>API Key is required</AlertDialogTitle>
|
||||
<AlertDialogDescription>Please enter your API key</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<form className="flex gap-2" onSubmit={(e) => e.preventDefault()}>
|
||||
<Input
|
||||
type="password"
|
||||
value={tempApiKey}
|
||||
onChange={handleTempApiKeyChange}
|
||||
placeholder="Enter your API key"
|
||||
className="max-h-full w-full min-w-0"
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
<Button onClick={setApiKey} variant="outline" size="sm">
|
||||
Save
|
||||
</Button>
|
||||
</form>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default ApiKeyAlert
|
@@ -40,7 +40,7 @@ const LabeledCheckBox = ({
|
||||
*/
|
||||
export default function Settings() {
|
||||
const [opened, setOpened] = useState<boolean>(false)
|
||||
const [tempApiKey, setTempApiKey] = useState<string>('') // 用于临时存储输入的API Key
|
||||
const [tempApiKey, setTempApiKey] = useState<string>('')
|
||||
|
||||
const showPropertyPanel = useSettingsStore.use.showPropertyPanel()
|
||||
const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar()
|
@@ -2,7 +2,7 @@ import { cn } from '@/lib/utils'
|
||||
import { useBackendState } from '@/stores/state'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/Popover'
|
||||
import StatusCard from '@/components/StatusCard'
|
||||
import StatusCard from '@/components/graph/StatusCard'
|
||||
|
||||
const StatusIndicator = () => {
|
||||
const health = useBackendState.use.health()
|
115
lightrag_webui/src/components/ui/AlertDialog.tsx
Normal file
115
lightrag_webui/src/components/ui/AlertDialog.tsx
Normal file
@@ -0,0 +1,115 @@
|
||||
import * as React from 'react'
|
||||
import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog'
|
||||
|
||||
import { cn } from '@/lib/utils'
|
||||
import { buttonVariants } from '@/components/ui/Button'
|
||||
|
||||
const AlertDialog = AlertDialogPrimitive.Root
|
||||
|
||||
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
|
||||
|
||||
const AlertDialogPortal = AlertDialogPrimitive.Portal
|
||||
|
||||
const AlertDialogOverlay = React.forwardRef<
|
||||
React.ComponentRef<typeof AlertDialogPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
|
||||
|
||||
const AlertDialogContent = React.forwardRef<
|
||||
React.ComponentRef<typeof AlertDialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay />
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border p-6 shadow-lg duration-200 sm:rounded-lg',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</AlertDialogPortal>
|
||||
))
|
||||
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
|
||||
|
||||
const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div className={cn('flex flex-col space-y-2 text-center sm:text-left', className)} {...props} />
|
||||
)
|
||||
AlertDialogHeader.displayName = 'AlertDialogHeader'
|
||||
|
||||
const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn('flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
AlertDialogFooter.displayName = 'AlertDialogFooter'
|
||||
|
||||
const AlertDialogTitle = React.forwardRef<
|
||||
React.ComponentRef<typeof AlertDialogPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
|
||||
|
||||
const AlertDialogDescription = React.forwardRef<
|
||||
React.ComponentRef<typeof AlertDialogPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
|
||||
|
||||
const AlertDialogAction = React.forwardRef<
|
||||
React.ComponentRef<typeof AlertDialogPrimitive.Action>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Action ref={ref} className={cn(buttonVariants(), className)} {...props} />
|
||||
))
|
||||
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
||||
|
||||
const AlertDialogCancel = React.forwardRef<
|
||||
React.ComponentRef<typeof AlertDialogPrimitive.Cancel>,
|
||||
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Cancel
|
||||
ref={ref}
|
||||
className={cn(buttonVariants({ variant: 'outline' }), 'mt-2 sm:mt-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
||||
|
||||
export {
|
||||
AlertDialog,
|
||||
AlertDialogPortal,
|
||||
AlertDialogOverlay,
|
||||
AlertDialogTrigger,
|
||||
AlertDialogContent,
|
||||
AlertDialogHeader,
|
||||
AlertDialogFooter,
|
||||
AlertDialogTitle,
|
||||
AlertDialogDescription,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel
|
||||
}
|
@@ -4,7 +4,8 @@ import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/Tooltip'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
// eslint-disable-next-line react-refresh/only-export-components
|
||||
export const buttonVariants = cva(
|
||||
'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
|
@@ -7,16 +7,16 @@ import { EdgeArrowProgram, NodePointProgram, NodeCircleProgram } from 'sigma/ren
|
||||
import { NodeBorderProgram } from '@sigma/node-border'
|
||||
import EdgeCurveProgram, { EdgeCurvedArrowProgram } from '@sigma/edge-curve'
|
||||
|
||||
import FocusOnNode from '@/components/FocusOnNode'
|
||||
import LayoutsControl from '@/components/LayoutsControl'
|
||||
import GraphControl from '@/components/GraphControl'
|
||||
import FocusOnNode from '@/components/graph/FocusOnNode'
|
||||
import LayoutsControl from '@/components/graph/LayoutsControl'
|
||||
import GraphControl from '@/components/graph/GraphControl'
|
||||
// import ThemeToggle from '@/components/ThemeToggle'
|
||||
import ZoomControl from '@/components/ZoomControl'
|
||||
import FullScreenControl from '@/components/FullScreenControl'
|
||||
import Settings from '@/components/Settings'
|
||||
import GraphSearch from '@/components/GraphSearch'
|
||||
import GraphLabels from '@/components/GraphLabels'
|
||||
import PropertiesView from '@/components/PropertiesView'
|
||||
import ZoomControl from '@/components/graph/ZoomControl'
|
||||
import FullScreenControl from '@/components/graph/FullScreenControl'
|
||||
import Settings from '@/components/graph/Settings'
|
||||
import GraphSearch from '@/components/graph/GraphSearch'
|
||||
import GraphLabels from '@/components/graph/GraphLabels'
|
||||
import PropertiesView from '@/components/graph/PropertiesView'
|
||||
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { useGraphStore } from '@/stores/graph'
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import Button from '@/components/ui/Button'
|
||||
import { SiteInfo } from '@/lib/constants'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import ThemeToggle from '@/components/graph/ThemeToggle'
|
||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||
import { useSettingsStore } from '@/stores/settings'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { useContext } from 'react'
|
||||
import { ThemeProviderContext } from '@/components/ThemeProvider'
|
||||
import { ThemeProviderContext } from '@/components/graph/ThemeProvider'
|
||||
|
||||
const useTheme = () => {
|
||||
const context = useContext(ThemeProviderContext)
|
||||
|
Reference in New Issue
Block a user