+ // eslint-disable-next-line react/prop-types
+>(({ className, ...props }, ref) => (
+ [role=checkbox]]:translate-y-[2px]',
+ className
+ )}
+ {...props}
+ />
+))
+TableCell.displayName = 'TableCell'
+
+const TableCaption = React.forwardRef<
+ HTMLTableCaptionElement,
+ React.HTMLAttributes
+>(({ className, ...props }, ref) => (
+
+))
+TableCaption.displayName = 'TableCaption'
+
+export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
diff --git a/lightrag_webui/src/components/ui/Tabs.tsx b/lightrag_webui/src/components/ui/Tabs.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..87df84be5c54e65f07e08fdc2e09db25504fa432
--- /dev/null
+++ b/lightrag_webui/src/components/ui/Tabs.tsx
@@ -0,0 +1,53 @@
+import * as React from 'react'
+import * as TabsPrimitive from '@radix-ui/react-tabs'
+
+import { cn } from '@/lib/utils'
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+ React.ComponentRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }
diff --git a/lightrag_webui/src/components/ui/Tooltip.tsx b/lightrag_webui/src/components/ui/Tooltip.tsx
index 538a925cef5802aef6e5ec8bc2015ffc6837dce6..674ddd501328e7ceedcc9a894dafd64a468275b7 100644
--- a/lightrag_webui/src/components/ui/Tooltip.tsx
+++ b/lightrag_webui/src/components/ui/Tooltip.tsx
@@ -8,19 +8,31 @@ const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
+const processTooltipContent = (content: string) => {
+ if (typeof content !== 'string') return content
+ return content.split('\\n').map((line, i) => (
+
+ {line}
+ {i < content.split('\\n').length - 1 && }
+
+ ))
+}
+
const TooltipContent = React.forwardRef<
React.ComponentRef,
React.ComponentPropsWithoutRef
->(({ className, sideOffset = 4, ...props }, ref) => (
+>(({ className, sideOffset = 4, children, ...props }, ref) => (
+ >
+ {typeof children === 'string' ? processTooltipContent(children) : children}
+
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
diff --git a/lightrag_webui/src/features/ApiSite.tsx b/lightrag_webui/src/features/ApiSite.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..fa9e263f387b9f7edd0592356a148c49d2de1624
--- /dev/null
+++ b/lightrag_webui/src/features/ApiSite.tsx
@@ -0,0 +1,5 @@
+import { backendBaseUrl } from '@/lib/constants'
+
+export default function ApiSite() {
+ return
+}
diff --git a/lightrag_webui/src/features/DocumentManager.tsx b/lightrag_webui/src/features/DocumentManager.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0aa068d4af16893bd1250b8e097265ce83e95512
--- /dev/null
+++ b/lightrag_webui/src/features/DocumentManager.tsx
@@ -0,0 +1,166 @@
+import { useState, useEffect, useCallback } from 'react'
+import Button from '@/components/ui/Button'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow
+} from '@/components/ui/Table'
+import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@/components/ui/Card'
+import EmptyCard from '@/components/ui/EmptyCard'
+import Text from '@/components/ui/Text'
+import UploadDocumentsDialog from '@/components/documents/UploadDocumentsDialog'
+import ClearDocumentsDialog from '@/components/documents/ClearDocumentsDialog'
+
+import { getDocuments, scanNewDocuments, DocsStatusesResponse } from '@/api/lightrag'
+import { errorMessage } from '@/lib/utils'
+import { toast } from 'sonner'
+import { useBackendState } from '@/stores/state'
+
+import { RefreshCwIcon } from 'lucide-react'
+
+export default function DocumentManager() {
+ const health = useBackendState.use.health()
+ const [docs, setDocs] = useState(null)
+
+ const fetchDocuments = useCallback(async () => {
+ try {
+ const docs = await getDocuments()
+ if (docs && docs.statuses) {
+ setDocs(docs)
+ // console.log(docs)
+ } else {
+ setDocs(null)
+ }
+ } catch (err) {
+ toast.error('Failed to load documents\n' + errorMessage(err))
+ }
+ }, [setDocs])
+
+ useEffect(() => {
+ fetchDocuments()
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
+
+ const scanDocuments = useCallback(async () => {
+ try {
+ const { status } = await scanNewDocuments()
+ toast.message(status)
+ } catch (err) {
+ toast.error('Failed to load documents\n' + errorMessage(err))
+ }
+ }, [])
+
+ useEffect(() => {
+ const interval = setInterval(async () => {
+ if (!health) {
+ return
+ }
+ try {
+ await fetchDocuments()
+ } catch (err) {
+ toast.error('Failed to get scan progress\n' + errorMessage(err))
+ }
+ }, 5000)
+ return () => clearInterval(interval)
+ }, [health, fetchDocuments])
+
+ return (
+
+
+ Document Management
+
+
+
+
+
+
+ Uploaded documents
+ view the uploaded documents here
+
+
+
+ {!docs && (
+
+ )}
+ {docs && (
+
+
+
+ ID
+ Summary
+ Status
+ Length
+ Chunks
+ Created
+ Updated
+ Metadata
+
+
+
+ {Object.entries(docs.statuses).map(([status, documents]) =>
+ documents.map((doc) => (
+
+ {doc.id}
+
+
+
+
+ {status === 'processed' && (
+ Completed
+ )}
+ {status === 'processing' && (
+ Processing
+ )}
+ {status === 'pending' && Pending}
+ {status === 'failed' && Failed}
+ {doc.error && (
+
+ ⚠️
+
+ )}
+
+ {doc.content_length ?? '-'}
+ {doc.chunks_count ?? '-'}
+
+ {new Date(doc.created_at).toLocaleString()}
+
+
+ {new Date(doc.updated_at).toLocaleString()}
+
+
+ {doc.metadata ? JSON.stringify(doc.metadata) : '-'}
+
+
+ ))
+ )}
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/lightrag_webui/src/GraphViewer.tsx b/lightrag_webui/src/features/GraphViewer.tsx
similarity index 90%
rename from lightrag_webui/src/GraphViewer.tsx
rename to lightrag_webui/src/features/GraphViewer.tsx
index cfdc17098c24f9671ffa2ac8a658bdf0809e1903..6d1979c5bfc1aa1658c7f034fae751ed6ea7b4a2 100644
--- a/lightrag_webui/src/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 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 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/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'
@@ -166,7 +166,7 @@ const GraphViewer = () => {
-
+ {/* */}
{showPropertyPanel && (
diff --git a/lightrag_webui/src/features/RetrievalTesting.tsx b/lightrag_webui/src/features/RetrievalTesting.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e293322ba1523993e45e9fbcb69ad5e20f7986a6
--- /dev/null
+++ b/lightrag_webui/src/features/RetrievalTesting.tsx
@@ -0,0 +1,161 @@
+import Input from '@/components/ui/Input'
+import Button from '@/components/ui/Button'
+import { useCallback, useEffect, useRef, useState } from 'react'
+import { queryText, queryTextStream, Message } from '@/api/lightrag'
+import { errorMessage } from '@/lib/utils'
+import { useSettingsStore } from '@/stores/settings'
+import { useDebounce } from '@/hooks/useDebounce'
+import QuerySettings from '@/components/retrieval/QuerySettings'
+
+import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react'
+
+export default function RetrievalTesting() {
+ const [messages, setMessages] = useState(
+ () => useSettingsStore.getState().retrievalHistory || []
+ )
+ const [inputValue, setInputValue] = useState('')
+ const [isLoading, setIsLoading] = useState(false)
+ const messagesEndRef = useRef(null)
+
+ const scrollToBottom = useCallback(() => {
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
+ }, [])
+
+ const handleSubmit = useCallback(
+ async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!inputValue.trim() || isLoading) return
+
+ // Create messages
+ const userMessage: Message = {
+ content: inputValue,
+ role: 'user'
+ }
+
+ const assistantMessage: Message = {
+ content: '',
+ role: 'assistant'
+ }
+
+ const prevMessages = [...messages]
+
+ // Add messages to chatbox
+ setMessages([...prevMessages, userMessage, assistantMessage])
+
+ // Clear input and set loading
+ setInputValue('')
+ setIsLoading(true)
+
+ // Create a function to update the assistant's message
+ const updateAssistantMessage = (chunk: string) => {
+ assistantMessage.content += chunk
+ setMessages((prev) => {
+ const newMessages = [...prev]
+ const lastMessage = newMessages[newMessages.length - 1]
+ if (lastMessage.role === 'assistant') {
+ lastMessage.content = assistantMessage.content
+ }
+ return newMessages
+ })
+ }
+
+ // Prepare query parameters
+ const state = useSettingsStore.getState()
+ const queryParams = {
+ ...state.querySettings,
+ query: userMessage.content,
+ conversation_history: prevMessages
+ }
+
+ try {
+ // Run query
+ if (state.querySettings.stream) {
+ await queryTextStream(queryParams, updateAssistantMessage)
+ } else {
+ const response = await queryText(queryParams)
+ updateAssistantMessage(response.response)
+ }
+ } catch (err) {
+ // Handle error
+ updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`)
+ } finally {
+ // Clear loading and add messages to state
+ setIsLoading(false)
+ useSettingsStore
+ .getState()
+ .setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
+ }
+ },
+ [inputValue, isLoading, messages, setMessages]
+ )
+
+ const debouncedMessages = useDebounce(messages, 100)
+ useEffect(() => scrollToBottom(), [debouncedMessages, scrollToBottom])
+
+ const clearMessages = useCallback(() => {
+ setMessages([])
+ useSettingsStore.getState().setRetrievalHistory([])
+ }, [setMessages])
+
+ return (
+
+
+
+
+
+ {messages.length === 0 ? (
+
+ Start a retrieval by typing your query below
+
+ ) : (
+ messages.map((message, idx) => (
+
+
+ {message.content}
+ {message.content.length === 0 && (
+
+ )}
+
+
+ ))
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/lightrag_webui/src/features/SiteHeader.tsx b/lightrag_webui/src/features/SiteHeader.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..5446e382807d1caf2461d56911e5eee10ced0361
--- /dev/null
+++ b/lightrag_webui/src/features/SiteHeader.tsx
@@ -0,0 +1,75 @@
+import Button from '@/components/ui/Button'
+import { SiteInfo } from '@/lib/constants'
+import ThemeToggle from '@/components/graph/ThemeToggle'
+import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
+import { useSettingsStore } from '@/stores/settings'
+import { cn } from '@/lib/utils'
+
+import { ZapIcon, GithubIcon } from 'lucide-react'
+
+interface NavigationTabProps {
+ value: string
+ currentTab: string
+ children: React.ReactNode
+}
+
+function NavigationTab({ value, currentTab, children }: NavigationTabProps) {
+ return (
+
+ {children}
+
+ )
+}
+
+function TabsNavigation() {
+ const currentTab = useSettingsStore.use.currentTab()
+
+ return (
+
+
+
+ Documents
+
+
+ Knowledge Graph
+
+
+ Retrieval
+
+
+ API
+
+
+
+ )
+}
+
+export default function SiteHeader() {
+ return (
+
+ )
+}
diff --git a/lightrag_webui/src/hooks/useTheme.tsx b/lightrag_webui/src/hooks/useTheme.tsx
index 5fb161a4600d1a37715381b9d8a2667b4e736d21..e19751745ea9ea50553e8c7f51f2f56e9310dd1b 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)
diff --git a/lightrag_webui/src/index.css b/lightrag_webui/src/index.css
index 04deb479bd015034213788083f98a52c899bd869..d4413cec9b3f482659fc3dc1bdf4db1a5fe43be2 100644
--- a/lightrag_webui/src/index.css
+++ b/lightrag_webui/src/index.css
@@ -1,6 +1,7 @@
@import 'tailwindcss';
@plugin 'tailwindcss-animate';
+@plugin 'tailwind-scrollbar';
@custom-variant dark (&:is(.dark *));
@@ -142,3 +143,27 @@
@apply bg-background text-foreground;
}
}
+
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+
+::-webkit-scrollbar-thumb {
+ background-color: hsl(0 0% 80%);
+ border-radius: 5px;
+}
+
+::-webkit-scrollbar-track {
+ background-color: hsl(0 0% 95%);
+}
+
+.dark {
+ ::-webkit-scrollbar-thumb {
+ background-color: hsl(0 0% 90%);
+ }
+
+ ::-webkit-scrollbar-track {
+ background-color: hsl(0 0% 0%);
+ }
+}
diff --git a/lightrag_webui/src/lib/constants.ts b/lightrag_webui/src/lib/constants.ts
index c7b59cbdbcb24ffe73c0294fbdcaf15fce414c11..9f972a3561c85066a1988664d3a662bf0af45cf6 100644
--- a/lightrag_webui/src/lib/constants.ts
+++ b/lightrag_webui/src/lib/constants.ts
@@ -23,3 +23,18 @@ export const maxNodeSize = 20
export const healthCheckInterval = 15 // seconds
export const defaultQueryLabel = '*'
+
+// reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/MIME_types/Common_types
+export const supportedFileTypes = {
+ 'text/plain': ['.txt', '.md'],
+ 'application/pdf': ['.pdf'],
+ 'application/msword': ['.doc'],
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'],
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation': ['.pptx']
+}
+
+export const SiteInfo = {
+ name: 'LightRAG',
+ home: '/',
+ github: 'https://github.com/HKUDS/LightRAG'
+}
diff --git a/lightrag_webui/src/stores/settings.ts b/lightrag_webui/src/stores/settings.ts
index 38d57170ac4b1346acb35af871486b941aa00ac4..84f5feac2a21651e98e84d5dbd48b7c63e2b65c2 100644
--- a/lightrag_webui/src/stores/settings.ts
+++ b/lightrag_webui/src/stores/settings.ts
@@ -2,8 +2,10 @@ import { create } from 'zustand'
import { persist, createJSONStorage } from 'zustand/middleware'
import { createSelectors } from '@/lib/utils'
import { defaultQueryLabel } from '@/lib/constants'
+import { Message, QueryRequest } from '@/api/lightrag'
type Theme = 'dark' | 'light' | 'system'
+type Tab = 'documents' | 'knowledge-graph' | 'retrieval'
interface SettingsState {
theme: Theme
@@ -27,6 +29,15 @@ interface SettingsState {
apiKey: string | null
setApiKey: (key: string | null) => void
+
+ currentTab: Tab
+ setCurrentTab: (tab: Tab) => void
+
+ retrievalHistory: Message[]
+ setRetrievalHistory: (history: Message[]) => void
+
+ querySettings: Omit
+ updateQuerySettings: (settings: Partial) => void
}
const useSettingsStoreBase = create()(
@@ -49,6 +60,25 @@ const useSettingsStoreBase = create()(
apiKey: null,
+ currentTab: 'documents',
+
+ retrievalHistory: [],
+
+ querySettings: {
+ mode: 'global',
+ response_type: 'Multiple Paragraphs',
+ top_k: 10,
+ max_token_for_text_unit: 4000,
+ max_token_for_global_context: 4000,
+ max_token_for_local_context: 4000,
+ only_need_context: false,
+ only_need_prompt: false,
+ stream: true,
+ history_turns: 3,
+ hl_keywords: [],
+ ll_keywords: []
+ },
+
setTheme: (theme: Theme) => set({ theme }),
setQueryLabel: (queryLabel: string) =>
@@ -58,12 +88,21 @@ const useSettingsStoreBase = create()(
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
- setApiKey: (apiKey: string | null) => set({ apiKey })
+ setApiKey: (apiKey: string | null) => set({ apiKey }),
+
+ setCurrentTab: (tab: Tab) => set({ currentTab: tab }),
+
+ setRetrievalHistory: (history: Message[]) => set({ retrievalHistory: history }),
+
+ updateQuerySettings: (settings: Partial) =>
+ set((state) => ({
+ querySettings: { ...state.querySettings, ...settings }
+ }))
}),
{
name: 'settings-storage',
storage: createJSONStorage(() => localStorage),
- version: 4,
+ version: 6,
migrate: (state: any, version: number) => {
if (version < 2) {
state.showEdgeLabel = false
@@ -78,6 +117,27 @@ const useSettingsStoreBase = create()(
state.enableHealthCheck = true
state.apiKey = null
}
+ if (version < 5) {
+ state.currentTab = 'documents'
+ }
+ if (version < 6) {
+ state.querySettings = {
+ mode: 'global',
+ response_type: 'Multiple Paragraphs',
+ top_k: 10,
+ max_token_for_text_unit: 4000,
+ max_token_for_global_context: 4000,
+ max_token_for_local_context: 4000,
+ only_need_context: false,
+ only_need_prompt: false,
+ stream: true,
+ history_turns: 3,
+ hl_keywords: [],
+ ll_keywords: []
+ }
+ state.retrievalHistory = []
+ }
+ return state
}
}
)
|