diff --git a/lightrag_webui/README.md b/lightrag_webui/README.md index 0a0ac346..ac27f880 100644 --- a/lightrag_webui/README.md +++ b/lightrag_webui/README.md @@ -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. diff --git a/lightrag_webui/bun.lock b/lightrag_webui/bun.lock index d0e9a9b4..570fa3a6 100644 --- a/lightrag_webui/bun.lock +++ b/lightrag_webui/bun.lock @@ -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=="], diff --git a/lightrag_webui/package.json b/lightrag_webui/package.json index 6d720cc8..26991d9c 100644 --- a/lightrag_webui/package.json +++ b/lightrag_webui/package.json @@ -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", diff --git a/lightrag_webui/src/App.tsx b/lightrag_webui/src/App.tsx index 1d63b3c1..1eeab2b0 100644 --- a/lightrag_webui/src/App.tsx +++ b/lightrag_webui/src/App.tsx @@ -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 (
@@ -64,7 +75,8 @@ function App() { {enableHealthCheck && } - {message !== null && } + {message !== null && !apiKeyInvalid && } + {apiKeyInvalid && }
diff --git a/lightrag_webui/src/components/ApiKeyAlert.tsx b/lightrag_webui/src/components/ApiKeyAlert.tsx new file mode 100644 index 00000000..f0276085 --- /dev/null +++ b/lightrag_webui/src/components/ApiKeyAlert.tsx @@ -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(true) + const apiKey = useSettingsStore.use.apiKey() + const [tempApiKey, setTempApiKey] = useState('') + 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) => { + setTempApiKey(e.target.value) + }, + [setTempApiKey] + ) + + return ( + + Open + + + API Key is required + Please enter your API key + +
e.preventDefault()}> + + + +
+
+
+ ) +} + +export default ApiKeyAlert diff --git a/lightrag_webui/src/components/FocusOnNode.tsx b/lightrag_webui/src/components/graph/FocusOnNode.tsx similarity index 100% rename from lightrag_webui/src/components/FocusOnNode.tsx rename to lightrag_webui/src/components/graph/FocusOnNode.tsx diff --git a/lightrag_webui/src/components/FullScreenControl.tsx b/lightrag_webui/src/components/graph/FullScreenControl.tsx similarity index 100% rename from lightrag_webui/src/components/FullScreenControl.tsx rename to lightrag_webui/src/components/graph/FullScreenControl.tsx diff --git a/lightrag_webui/src/components/GraphControl.tsx b/lightrag_webui/src/components/graph/GraphControl.tsx similarity index 100% rename from lightrag_webui/src/components/GraphControl.tsx rename to lightrag_webui/src/components/graph/GraphControl.tsx diff --git a/lightrag_webui/src/components/GraphLabels.tsx b/lightrag_webui/src/components/graph/GraphLabels.tsx similarity index 100% rename from lightrag_webui/src/components/GraphLabels.tsx rename to lightrag_webui/src/components/graph/GraphLabels.tsx diff --git a/lightrag_webui/src/components/GraphSearch.tsx b/lightrag_webui/src/components/graph/GraphSearch.tsx similarity index 100% rename from lightrag_webui/src/components/GraphSearch.tsx rename to lightrag_webui/src/components/graph/GraphSearch.tsx diff --git a/lightrag_webui/src/components/LayoutsControl.tsx b/lightrag_webui/src/components/graph/LayoutsControl.tsx similarity index 100% rename from lightrag_webui/src/components/LayoutsControl.tsx rename to lightrag_webui/src/components/graph/LayoutsControl.tsx diff --git a/lightrag_webui/src/components/PropertiesView.tsx b/lightrag_webui/src/components/graph/PropertiesView.tsx similarity index 100% rename from lightrag_webui/src/components/PropertiesView.tsx rename to lightrag_webui/src/components/graph/PropertiesView.tsx diff --git a/lightrag_webui/src/components/Settings.tsx b/lightrag_webui/src/components/graph/Settings.tsx similarity index 98% rename from lightrag_webui/src/components/Settings.tsx rename to lightrag_webui/src/components/graph/Settings.tsx index 2c7dd733..3a6cc51c 100644 --- a/lightrag_webui/src/components/Settings.tsx +++ b/lightrag_webui/src/components/graph/Settings.tsx @@ -40,7 +40,7 @@ const LabeledCheckBox = ({ */ export default function Settings() { const [opened, setOpened] = useState(false) - const [tempApiKey, setTempApiKey] = useState('') // 用于临时存储输入的API Key + const [tempApiKey, setTempApiKey] = useState('') const showPropertyPanel = useSettingsStore.use.showPropertyPanel() const showNodeSearchBar = useSettingsStore.use.showNodeSearchBar() diff --git a/lightrag_webui/src/components/StatusCard.tsx b/lightrag_webui/src/components/graph/StatusCard.tsx similarity index 100% rename from lightrag_webui/src/components/StatusCard.tsx rename to lightrag_webui/src/components/graph/StatusCard.tsx diff --git a/lightrag_webui/src/components/StatusIndicator.tsx b/lightrag_webui/src/components/graph/StatusIndicator.tsx similarity index 96% rename from lightrag_webui/src/components/StatusIndicator.tsx rename to lightrag_webui/src/components/graph/StatusIndicator.tsx index 25d0032c..3272d9fa 100644 --- a/lightrag_webui/src/components/StatusIndicator.tsx +++ b/lightrag_webui/src/components/graph/StatusIndicator.tsx @@ -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() diff --git a/lightrag_webui/src/components/ThemeProvider.tsx b/lightrag_webui/src/components/graph/ThemeProvider.tsx similarity index 100% rename from lightrag_webui/src/components/ThemeProvider.tsx rename to lightrag_webui/src/components/graph/ThemeProvider.tsx diff --git a/lightrag_webui/src/components/ThemeToggle.tsx b/lightrag_webui/src/components/graph/ThemeToggle.tsx similarity index 100% rename from lightrag_webui/src/components/ThemeToggle.tsx rename to lightrag_webui/src/components/graph/ThemeToggle.tsx diff --git a/lightrag_webui/src/components/ZoomControl.tsx b/lightrag_webui/src/components/graph/ZoomControl.tsx similarity index 100% rename from lightrag_webui/src/components/ZoomControl.tsx rename to lightrag_webui/src/components/graph/ZoomControl.tsx diff --git a/lightrag_webui/src/components/ui/AlertDialog.tsx b/lightrag_webui/src/components/ui/AlertDialog.tsx new file mode 100644 index 00000000..36868fcb --- /dev/null +++ b/lightrag_webui/src/components/ui/AlertDialog.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = 'AlertDialogHeader' + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = 'AlertDialogFooter' + +const AlertDialogTitle = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ComponentRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel +} diff --git a/lightrag_webui/src/components/ui/Button.tsx b/lightrag_webui/src/components/ui/Button.tsx index e7e961bc..070e8375 100644 --- a/lightrag_webui/src/components/ui/Button.tsx +++ b/lightrag_webui/src/components/ui/Button.tsx @@ -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: { diff --git a/lightrag_webui/src/features/GraphViewer.tsx b/lightrag_webui/src/features/GraphViewer.tsx index d5974c41..6d1979c5 100644 --- a/lightrag_webui/src/features/GraphViewer.tsx +++ b/lightrag_webui/src/features/GraphViewer.tsx @@ -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' diff --git a/lightrag_webui/src/features/SiteHeader.tsx b/lightrag_webui/src/features/SiteHeader.tsx index 6eef0bb9..5446e382 100644 --- a/lightrag_webui/src/features/SiteHeader.tsx +++ b/lightrag_webui/src/features/SiteHeader.tsx @@ -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' diff --git a/lightrag_webui/src/hooks/useTheme.tsx b/lightrag_webui/src/hooks/useTheme.tsx index 5fb161a4..e1975174 100644 --- a/lightrag_webui/src/hooks/useTheme.tsx +++ b/lightrag_webui/src/hooks/useTheme.tsx @@ -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)