ArnoChen commited on
Commit
29ee1c2
·
1 Parent(s): 0e5f52f

enhance web UI with retrieval testing and UI improvements

Browse files
lightrag_webui/eslint.config.js CHANGED
@@ -10,7 +10,7 @@ import react from 'eslint-plugin-react'
10
  export default tseslint.config(
11
  { ignores: ['dist'] },
12
  {
13
- extends: [js.configs.recommended, ...tseslint.configs.recommended],
14
  files: ['**/*.{ts,tsx,js,jsx}'],
15
  languageOptions: {
16
  ecmaVersion: 2020,
@@ -31,7 +31,6 @@ export default tseslint.config(
31
  '@stylistic/js/indent': ['error', 2],
32
  '@stylistic/js/quotes': ['error', 'single'],
33
  '@typescript-eslint/no-explicit-any': ['off']
34
- },
35
- prettier
36
  }
37
  )
 
10
  export default tseslint.config(
11
  { ignores: ['dist'] },
12
  {
13
+ extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
14
  files: ['**/*.{ts,tsx,js,jsx}'],
15
  languageOptions: {
16
  ecmaVersion: 2020,
 
31
  '@stylistic/js/indent': ['error', 2],
32
  '@stylistic/js/quotes': ['error', 'single'],
33
  '@typescript-eslint/no-explicit-any': ['off']
34
+ }
 
35
  }
36
  )
lightrag_webui/src/App.tsx CHANGED
@@ -1,3 +1,4 @@
 
1
  import ThemeProvider from '@/components/ThemeProvider'
2
  import MessageAlert from '@/components/MessageAlert'
3
  import StatusIndicator from '@/components/StatusIndicator'
@@ -10,14 +11,16 @@ import SiteHeader from '@/features/SiteHeader'
10
 
11
  import GraphViewer from '@/features/GraphViewer'
12
  import DocumentManager from '@/features/DocumentManager'
 
13
 
14
  import { Tabs, TabsContent } from '@/components/ui/Tabs'
15
 
16
  function App() {
17
  const message = useBackendState.use.message()
18
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
 
19
 
20
- // health check
21
  useEffect(() => {
22
  if (!enableHealthCheck) return
23
 
@@ -30,25 +33,36 @@ function App() {
30
  return () => clearInterval(interval)
31
  }, [enableHealthCheck])
32
 
 
 
 
 
 
33
  return (
34
  <ThemeProvider>
35
- <div className="flex h-screen w-screen">
36
- <Tabs defaultValue="knowledge-graph" className="flex size-full flex-col">
 
 
 
 
37
  <SiteHeader />
38
- <TabsContent value="documents" className="flex-1">
39
- <DocumentManager />
40
- </TabsContent>
41
- <TabsContent value="knowledge-graph" className="flex-1">
42
- <GraphViewer />
43
- </TabsContent>
44
- <TabsContent value="settings" className="size-full">
45
- <h1> Settings </h1>
46
- </TabsContent>
 
 
47
  </Tabs>
48
- </div>
49
- {enableHealthCheck && <StatusIndicator />}
50
- {message !== null && <MessageAlert />}
51
- <Toaster />
52
  </ThemeProvider>
53
  )
54
  }
 
1
+ import { useState, useCallback } from 'react'
2
  import ThemeProvider from '@/components/ThemeProvider'
3
  import MessageAlert from '@/components/MessageAlert'
4
  import StatusIndicator from '@/components/StatusIndicator'
 
11
 
12
  import GraphViewer from '@/features/GraphViewer'
13
  import DocumentManager from '@/features/DocumentManager'
14
+ import RetrievalTesting from '@/features/RetrievalTesting'
15
 
16
  import { Tabs, TabsContent } from '@/components/ui/Tabs'
17
 
18
  function App() {
19
  const message = useBackendState.use.message()
20
  const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
21
+ const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
22
 
23
+ // Health check
24
  useEffect(() => {
25
  if (!enableHealthCheck) return
26
 
 
33
  return () => clearInterval(interval)
34
  }, [enableHealthCheck])
35
 
36
+ const handleTabChange = useCallback(
37
+ (tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
38
+ []
39
+ )
40
+
41
  return (
42
  <ThemeProvider>
43
+ <main className="flex h-screen w-screen overflow-x-hidden">
44
+ <Tabs
45
+ defaultValue={currentTab}
46
+ className="!m-0 flex grow flex-col !p-0"
47
+ onValueChange={handleTabChange}
48
+ >
49
  <SiteHeader />
50
+ <div className="relative grow">
51
+ <TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
52
+ <DocumentManager />
53
+ </TabsContent>
54
+ <TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
55
+ <GraphViewer />
56
+ </TabsContent>
57
+ <TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
58
+ <RetrievalTesting />
59
+ </TabsContent>
60
+ </div>
61
  </Tabs>
62
+ {enableHealthCheck && <StatusIndicator />}
63
+ {message !== null && <MessageAlert />}
64
+ <Toaster />
65
+ </main>
66
  </ThemeProvider>
67
  )
68
  }
lightrag_webui/src/api/lightrag.ts CHANGED
@@ -151,32 +151,64 @@ export const queryText = async (request: QueryRequest): Promise<QueryResponse> =
151
  return response.data
152
  }
153
 
154
- export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => {
155
- const response = await axiosInstance.post('/query/stream', request, {
156
- responseType: 'stream'
157
- })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
 
159
- const reader = response.data.getReader()
160
- const decoder = new TextDecoder()
161
-
162
- while (true) {
163
- const { done, value } = await reader.read()
164
- if (done) break
165
-
166
- const chunk = decoder.decode(value)
167
- const lines = chunk.split('\n')
168
- for (const line of lines) {
169
- if (line) {
170
- try {
171
- const data = JSON.parse(line)
172
- if (data.response) {
173
- onChunk(data.response)
174
  }
175
- } catch (e) {
176
- console.error('Error parsing stream chunk:', e)
 
 
 
 
 
 
 
 
 
 
 
177
  }
 
 
 
178
  }
179
  }
 
 
 
 
180
  }
181
  }
182
 
@@ -199,6 +231,7 @@ export const uploadDocument = async (
199
  headers: {
200
  'Content-Type': 'multipart/form-data'
201
  },
 
202
  onUploadProgress:
203
  onUploadProgress !== undefined
204
  ? (progressEvent) => {
 
151
  return response.data
152
  }
153
 
154
+ export const queryTextStream = async (
155
+ request: QueryRequest,
156
+ onChunk: (chunk: string) => void,
157
+ onError?: (error: string) => void
158
+ ) => {
159
+ try {
160
+ let buffer = ''
161
+ await axiosInstance.post('/query/stream', request, {
162
+ responseType: 'text',
163
+ headers: {
164
+ Accept: 'application/x-ndjson'
165
+ },
166
+ transformResponse: [
167
+ (data: string) => {
168
+ // Accumulate the data and process complete lines
169
+ buffer += data
170
+ const lines = buffer.split('\n')
171
+ // Keep the last potentially incomplete line in the buffer
172
+ buffer = lines.pop() || ''
173
 
174
+ for (const line of lines) {
175
+ if (line.trim()) {
176
+ try {
177
+ const parsed = JSON.parse(line)
178
+ if (parsed.response) {
179
+ onChunk(parsed.response)
180
+ } else if (parsed.error && onError) {
181
+ onError(parsed.error)
182
+ }
183
+ } catch (e) {
184
+ console.error('Error parsing stream chunk:', e)
185
+ if (onError) onError('Error parsing server response')
186
+ }
187
+ }
 
188
  }
189
+ return data
190
+ }
191
+ ]
192
+ })
193
+
194
+ // Process any remaining data in the buffer
195
+ if (buffer.trim()) {
196
+ try {
197
+ const parsed = JSON.parse(buffer)
198
+ if (parsed.response) {
199
+ onChunk(parsed.response)
200
+ } else if (parsed.error && onError) {
201
+ onError(parsed.error)
202
  }
203
+ } catch (e) {
204
+ console.error('Error parsing final chunk:', e)
205
+ if (onError) onError('Error parsing server response')
206
  }
207
  }
208
+ } catch (error) {
209
+ const message = errorMessage(error)
210
+ console.error('Stream request failed:', message)
211
+ if (onError) onError(message)
212
  }
213
  }
214
 
 
231
  headers: {
232
  'Content-Type': 'multipart/form-data'
233
  },
234
+ // prettier-ignore
235
  onUploadProgress:
236
  onUploadProgress !== undefined
237
  ? (progressEvent) => {
lightrag_webui/src/components/ThemeToggle.tsx CHANGED
@@ -19,6 +19,7 @@ export default function ThemeToggle() {
19
  variant={controlButtonVariant}
20
  tooltip="Switch to light theme"
21
  size="icon"
 
22
  >
23
  <MoonIcon />
24
  </Button>
@@ -30,6 +31,7 @@ export default function ThemeToggle() {
30
  variant={controlButtonVariant}
31
  tooltip="Switch to dark theme"
32
  size="icon"
 
33
  >
34
  <SunIcon />
35
  </Button>
 
19
  variant={controlButtonVariant}
20
  tooltip="Switch to light theme"
21
  size="icon"
22
+ side="bottom"
23
  >
24
  <MoonIcon />
25
  </Button>
 
31
  variant={controlButtonVariant}
32
  tooltip="Switch to dark theme"
33
  size="icon"
34
+ side="bottom"
35
  >
36
  <SunIcon />
37
  </Button>
lightrag_webui/src/components/document/ClearDocumentsDialog.tsx CHANGED
@@ -15,7 +15,7 @@ import { clearDocuments } from '@/api/lightrag'
15
  import { EraserIcon } from 'lucide-react'
16
 
17
  export default function ClearDocumentsDialog() {
18
- const [open, setOpen] = useState(false) // 添加状态控制
19
 
20
  const handleClear = useCallback(async () => {
21
  try {
@@ -34,8 +34,8 @@ export default function ClearDocumentsDialog() {
34
  return (
35
  <Dialog open={open} onOpenChange={setOpen}>
36
  <DialogTrigger asChild>
37
- <Button variant="outline" tooltip="Clear documents" side="bottom" size="icon">
38
- <EraserIcon />
39
  </Button>
40
  </DialogTrigger>
41
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
 
15
  import { EraserIcon } from 'lucide-react'
16
 
17
  export default function ClearDocumentsDialog() {
18
+ const [open, setOpen] = useState(false)
19
 
20
  const handleClear = useCallback(async () => {
21
  try {
 
34
  return (
35
  <Dialog open={open} onOpenChange={setOpen}>
36
  <DialogTrigger asChild>
37
+ <Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
38
+ <EraserIcon/> Clear
39
  </Button>
40
  </DialogTrigger>
41
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
lightrag_webui/src/components/document/UploadDocumentsDialog.tsx CHANGED
@@ -66,8 +66,8 @@ export default function UploadDocumentsDialog() {
66
  }}
67
  >
68
  <DialogTrigger asChild>
69
- <Button variant="outline" tooltip="Upload documents" side="bottom" size="icon">
70
- <UploadIcon />
71
  </Button>
72
  </DialogTrigger>
73
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
 
66
  }}
67
  >
68
  <DialogTrigger asChild>
69
+ <Button variant="default" side="bottom" tooltip='Upload documents' size="sm">
70
+ <UploadIcon /> Upload
71
  </Button>
72
  </DialogTrigger>
73
  <DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
lightrag_webui/src/components/ui/Table.tsx CHANGED
@@ -56,6 +56,7 @@ TableRow.displayName = 'TableRow'
56
  const TableHead = React.forwardRef<
57
  HTMLTableCellElement,
58
  React.ThHTMLAttributes<HTMLTableCellElement>
 
59
  >(({ className, ...props }, ref) => (
60
  <th
61
  ref={ref}
@@ -71,6 +72,7 @@ TableHead.displayName = 'TableHead'
71
  const TableCell = React.forwardRef<
72
  HTMLTableCellElement,
73
  React.TdHTMLAttributes<HTMLTableCellElement>
 
74
  >(({ className, ...props }, ref) => (
75
  <td
76
  ref={ref}
 
56
  const TableHead = React.forwardRef<
57
  HTMLTableCellElement,
58
  React.ThHTMLAttributes<HTMLTableCellElement>
59
+ // eslint-disable-next-line react/prop-types
60
  >(({ className, ...props }, ref) => (
61
  <th
62
  ref={ref}
 
72
  const TableCell = React.forwardRef<
73
  HTMLTableCellElement,
74
  React.TdHTMLAttributes<HTMLTableCellElement>
75
+ // eslint-disable-next-line react/prop-types
76
  >(({ className, ...props }, ref) => (
77
  <td
78
  ref={ref}
lightrag_webui/src/features/DocumentManager.tsx CHANGED
@@ -16,23 +16,23 @@ import ClearDocumentsDialog from '@/components/document/ClearDocumentsDialog'
16
 
17
  import {
18
  getDocuments,
19
- getDocumentsScanProgress,
20
- scanNewDocuments,
21
- LightragDocumentsScanProgress
22
  } from '@/api/lightrag'
23
  import { errorMessage } from '@/lib/utils'
24
  import { toast } from 'sonner'
25
- import { useBackendState } from '@/stores/state'
26
 
27
  import { RefreshCwIcon, TrashIcon } from 'lucide-react'
28
 
29
  // type DocumentStatus = 'indexed' | 'pending' | 'indexing' | 'error'
30
 
31
  export default function DocumentManager() {
32
- const health = useBackendState.use.health()
33
  const [files, setFiles] = useState<string[]>([])
34
  const [indexedFiles, setIndexedFiles] = useState<string[]>([])
35
- const [scanProgress, setScanProgress] = useState<LightragDocumentsScanProgress | null>(null)
36
 
37
  const fetchDocuments = useCallback(async () => {
38
  try {
@@ -45,7 +45,7 @@ export default function DocumentManager() {
45
 
46
  useEffect(() => {
47
  fetchDocuments()
48
- }, [])
49
 
50
  const scanDocuments = useCallback(async () => {
51
  try {
@@ -54,26 +54,26 @@ export default function DocumentManager() {
54
  } catch (err) {
55
  toast.error('Failed to load documents\n' + errorMessage(err))
56
  }
57
- }, [setFiles])
58
 
59
- useEffect(() => {
60
- const interval = setInterval(async () => {
61
- try {
62
- if (!health) return
63
- const progress = await getDocumentsScanProgress()
64
- setScanProgress((pre) => {
65
- if (pre?.is_scanning === progress.is_scanning && progress.is_scanning === false) {
66
- return pre
67
- }
68
- return progress
69
- })
70
- console.log(progress)
71
- } catch (err) {
72
- toast.error('Failed to get scan progress\n' + errorMessage(err))
73
- }
74
- }, 2000)
75
- return () => clearInterval(interval)
76
- }, [health])
77
 
78
  const handleDelete = async (fileName: string) => {
79
  console.log(`deleting ${fileName}`)
@@ -88,19 +88,19 @@ export default function DocumentManager() {
88
  <div className="flex gap-2">
89
  <Button
90
  variant="outline"
91
- size="icon"
92
- tooltip="Scan Documents"
93
  onClick={scanDocuments}
94
  side="bottom"
 
 
95
  >
96
- <RefreshCwIcon />
97
  </Button>
98
  <div className="flex-1" />
99
  <ClearDocumentsDialog />
100
  <UploadDocumentsDialog />
101
  </div>
102
 
103
- {scanProgress?.is_scanning && (
104
  <div className="space-y-2">
105
  <div className="flex justify-between text-sm">
106
  <span>Indexing {scanProgress.current_file}</span>
@@ -108,7 +108,7 @@ export default function DocumentManager() {
108
  </div>
109
  <Progress value={scanProgress.progress} />
110
  </div>
111
- )}
112
 
113
  <Card>
114
  <CardHeader>
 
16
 
17
  import {
18
  getDocuments,
19
+ // getDocumentsScanProgress,
20
+ scanNewDocuments
21
+ // LightragDocumentsScanProgress
22
  } from '@/api/lightrag'
23
  import { errorMessage } from '@/lib/utils'
24
  import { toast } from 'sonner'
25
+ // import { useBackendState } from '@/stores/state'
26
 
27
  import { RefreshCwIcon, TrashIcon } from 'lucide-react'
28
 
29
  // type DocumentStatus = 'indexed' | 'pending' | 'indexing' | 'error'
30
 
31
  export default function DocumentManager() {
32
+ // const health = useBackendState.use.health()
33
  const [files, setFiles] = useState<string[]>([])
34
  const [indexedFiles, setIndexedFiles] = useState<string[]>([])
35
+ // const [scanProgress, setScanProgress] = useState<LightragDocumentsScanProgress | null>(null)
36
 
37
  const fetchDocuments = useCallback(async () => {
38
  try {
 
45
 
46
  useEffect(() => {
47
  fetchDocuments()
48
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
49
 
50
  const scanDocuments = useCallback(async () => {
51
  try {
 
54
  } catch (err) {
55
  toast.error('Failed to load documents\n' + errorMessage(err))
56
  }
57
+ }, [])
58
 
59
+ // useEffect(() => {
60
+ // const interval = setInterval(async () => {
61
+ // try {
62
+ // if (!health) return
63
+ // const progress = await getDocumentsScanProgress()
64
+ // setScanProgress((pre) => {
65
+ // if (pre?.is_scanning === progress.is_scanning && progress.is_scanning === false) {
66
+ // return pre
67
+ // }
68
+ // return progress
69
+ // })
70
+ // console.log(progress)
71
+ // } catch (err) {
72
+ // toast.error('Failed to get scan progress\n' + errorMessage(err))
73
+ // }
74
+ // }, 2000)
75
+ // return () => clearInterval(interval)
76
+ // }, [health])
77
 
78
  const handleDelete = async (fileName: string) => {
79
  console.log(`deleting ${fileName}`)
 
88
  <div className="flex gap-2">
89
  <Button
90
  variant="outline"
 
 
91
  onClick={scanDocuments}
92
  side="bottom"
93
+ tooltip="Scan documents"
94
+ size="sm"
95
  >
96
+ <RefreshCwIcon /> Scan
97
  </Button>
98
  <div className="flex-1" />
99
  <ClearDocumentsDialog />
100
  <UploadDocumentsDialog />
101
  </div>
102
 
103
+ {/* {scanProgress?.is_scanning && (
104
  <div className="space-y-2">
105
  <div className="flex justify-between text-sm">
106
  <span>Indexing {scanProgress.current_file}</span>
 
108
  </div>
109
  <Progress value={scanProgress.progress} />
110
  </div>
111
+ )} */}
112
 
113
  <Card>
114
  <CardHeader>
lightrag_webui/src/features/RetrievalTesting.tsx ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Input from '@/components/ui/Input'
2
+ import Button from '@/components/ui/Button'
3
+ import { useCallback, useEffect, useRef, useState } from 'react'
4
+ import { queryTextStream, QueryMode } from '@/api/lightrag'
5
+ import { errorMessage } from '@/lib/utils'
6
+ import { useSettingsStore } from '@/stores/settings'
7
+ import { useDebounce } from '@/hooks/useDebounce'
8
+ import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react'
9
+
10
+ type Message = {
11
+ id: string
12
+ content: string
13
+ role: 'User' | 'LightRAG'
14
+ }
15
+
16
+ export default function RetrievalTesting() {
17
+ const [messages, setMessages] = useState<Message[]>(
18
+ () => useSettingsStore.getState().retrievalHistory || []
19
+ )
20
+ const [inputValue, setInputValue] = useState('')
21
+ const [isLoading, setIsLoading] = useState(false)
22
+ const [mode, setMode] = useState<QueryMode>('mix')
23
+ const messagesEndRef = useRef<HTMLDivElement>(null)
24
+
25
+ const scrollToBottom = useCallback(() => {
26
+ messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
27
+ }, [])
28
+
29
+ const handleSubmit = useCallback(
30
+ async (e: React.FormEvent) => {
31
+ e.preventDefault()
32
+ if (!inputValue.trim() || isLoading) return
33
+
34
+ const userMessage: Message = {
35
+ id: Date.now().toString(),
36
+ content: inputValue,
37
+ role: 'User'
38
+ }
39
+
40
+ const assistantMessage: Message = {
41
+ id: (Date.now() + 1).toString(),
42
+ content: '',
43
+ role: 'LightRAG'
44
+ }
45
+
46
+ setMessages((prev) => {
47
+ const newMessages = [...prev, userMessage, assistantMessage]
48
+ return newMessages
49
+ })
50
+
51
+ setInputValue('')
52
+ setIsLoading(true)
53
+
54
+ // Create a function to update the assistant's message
55
+ const updateAssistantMessage = (chunk: string) => {
56
+ assistantMessage.content += chunk
57
+ setMessages((prev) => {
58
+ const newMessages = [...prev]
59
+ const lastMessage = newMessages[newMessages.length - 1]
60
+ if (lastMessage.role === 'LightRAG') {
61
+ lastMessage.content = assistantMessage.content
62
+ }
63
+ return newMessages
64
+ })
65
+ }
66
+
67
+ try {
68
+ await queryTextStream(
69
+ {
70
+ query: userMessage.content,
71
+ mode: mode,
72
+ stream: true
73
+ },
74
+ updateAssistantMessage
75
+ )
76
+ } catch (err) {
77
+ updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`)
78
+ } finally {
79
+ setIsLoading(false)
80
+ useSettingsStore
81
+ .getState()
82
+ .setRetrievalHistory([
83
+ ...useSettingsStore.getState().retrievalHistory,
84
+ userMessage,
85
+ assistantMessage
86
+ ])
87
+ }
88
+ },
89
+ [inputValue, isLoading, mode, setMessages]
90
+ )
91
+
92
+ const debouncedMessages = useDebounce(messages, 100)
93
+ useEffect(() => scrollToBottom(), [debouncedMessages, scrollToBottom])
94
+
95
+ const clearMessages = useCallback(() => {
96
+ setMessages([])
97
+ useSettingsStore.getState().setRetrievalHistory([])
98
+ }, [setMessages])
99
+
100
+ return (
101
+ <div className="flex size-full flex-col gap-4 px-32 py-6">
102
+ <div className="relative grow">
103
+ <div className="bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2">
104
+ <div className="flex min-h-0 flex-1 flex-col gap-2">
105
+ {messages.length === 0 ? (
106
+ <div className="text-muted-foreground flex h-full items-center justify-center text-lg">
107
+ Start a retrieval by typing your query below
108
+ </div>
109
+ ) : (
110
+ messages.map((message) => (
111
+ <div
112
+ key={message.id}
113
+ className={`flex ${message.role === 'User' ? 'justify-end' : 'justify-start'}`}
114
+ >
115
+ <div
116
+ className={`max-w-[80%] rounded-lg px-4 py-2 ${
117
+ message.role === 'User' ? 'bg-primary text-primary-foreground' : 'bg-muted'
118
+ }`}
119
+ >
120
+ <pre className="break-words whitespace-pre-wrap">{message.content}</pre>
121
+ {message.content.length === 0 && (
122
+ <LoaderIcon className="animate-spin duration-2000" />
123
+ )}
124
+ </div>
125
+ </div>
126
+ ))
127
+ )}
128
+ <div ref={messagesEndRef} className="pb-1" />
129
+ </div>
130
+ </div>
131
+ </div>
132
+
133
+ <form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2 pb-2">
134
+ <Button
135
+ type="button"
136
+ variant="outline"
137
+ onClick={clearMessages}
138
+ disabled={isLoading}
139
+ size="sm"
140
+ >
141
+ <EraserIcon />
142
+ Clear
143
+ </Button>
144
+ <select
145
+ className="border-input bg-background ring-offset-background h-9 rounded-md border px-3 py-1 text-sm"
146
+ value={mode}
147
+ onChange={(e) => setMode(e.target.value as QueryMode)}
148
+ disabled={isLoading}
149
+ >
150
+ <option value="naive">Naive</option>
151
+ <option value="local">Local</option>
152
+ <option value="global">Global</option>
153
+ <option value="hybrid">Hybrid</option>
154
+ <option value="mix">Mix</option>
155
+ </select>
156
+ <Input
157
+ className="flex-1"
158
+ value={inputValue}
159
+ onChange={(e) => setInputValue(e.target.value)}
160
+ placeholder="Type your query..."
161
+ disabled={isLoading}
162
+ />
163
+ <Button type="submit" variant="default" disabled={isLoading} size="sm">
164
+ <SendIcon />
165
+ Send
166
+ </Button>
167
+ </form>
168
+ </div>
169
+ )
170
+ }
lightrag_webui/src/features/SiteHeader.tsx CHANGED
@@ -2,14 +2,18 @@ import Button from '@/components/ui/Button'
2
  import { SiteInfo } from '@/lib/constants'
3
  import ThemeToggle from '@/components/ThemeToggle'
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
 
 
5
 
6
  import { ZapIcon, GithubIcon } from 'lucide-react'
7
 
8
  export default function SiteHeader() {
 
 
9
  return (
10
  <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">
11
  <a href="/" className="mr-6 flex items-center gap-2">
12
- <ZapIcon className="size-4 text-teal-400" aria-hidden="true" />
13
  <span className="font-bold md:inline-block">{SiteInfo.name}</span>
14
  </a>
15
 
@@ -18,28 +22,43 @@ export default function SiteHeader() {
18
  <TabsList className="h-full gap-2">
19
  <TabsTrigger
20
  value="documents"
21
- className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
 
 
 
 
 
22
  >
23
  Documents
24
  </TabsTrigger>
25
  <TabsTrigger
26
  value="knowledge-graph"
27
- className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
 
 
 
 
 
28
  >
29
  Knowledge Graph
30
  </TabsTrigger>
31
- {/* <TabsTrigger
32
- value="settings"
33
- className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
 
 
 
 
 
34
  >
35
- Settings
36
- </TabsTrigger> */}
37
  </TabsList>
38
  </div>
39
  </div>
40
 
41
  <nav className="flex items-center">
42
- <Button variant="ghost" size="icon">
43
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
44
  <GithubIcon className="size-4" aria-hidden="true" />
45
  </a>
 
2
  import { SiteInfo } from '@/lib/constants'
3
  import ThemeToggle from '@/components/ThemeToggle'
4
  import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
5
+ import { useSettingsStore } from '@/stores/settings'
6
+ import { cn } from '@/lib/utils'
7
 
8
  import { ZapIcon, GithubIcon } from 'lucide-react'
9
 
10
  export default function SiteHeader() {
11
+ const currentTab = useSettingsStore.use.currentTab()
12
+
13
  return (
14
  <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">
15
  <a href="/" className="mr-6 flex items-center gap-2">
16
+ <ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
17
  <span className="font-bold md:inline-block">{SiteInfo.name}</span>
18
  </a>
19
 
 
22
  <TabsList className="h-full gap-2">
23
  <TabsTrigger
24
  value="documents"
25
+ className={cn(
26
+ 'cursor-pointer px-2 py-1 transition-all',
27
+ currentTab === 'documents'
28
+ ? '!bg-emerald-400 !text-zinc-50'
29
+ : 'hover:bg-background/60'
30
+ )}
31
  >
32
  Documents
33
  </TabsTrigger>
34
  <TabsTrigger
35
  value="knowledge-graph"
36
+ className={cn(
37
+ 'cursor-pointer px-2 py-1 transition-all',
38
+ currentTab === 'knowledge-graph'
39
+ ? '!bg-emerald-400 !text-zinc-50'
40
+ : 'hover:bg-background/60'
41
+ )}
42
  >
43
  Knowledge Graph
44
  </TabsTrigger>
45
+ <TabsTrigger
46
+ value="retrieval"
47
+ className={cn(
48
+ 'cursor-pointer px-2 py-1 transition-all',
49
+ currentTab === 'retrieval'
50
+ ? '!bg-emerald-400 !text-zinc-50'
51
+ : 'hover:bg-background/60'
52
+ )}
53
  >
54
+ Retrieval
55
+ </TabsTrigger>
56
  </TabsList>
57
  </div>
58
  </div>
59
 
60
  <nav className="flex items-center">
61
+ <Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
62
  <a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
63
  <GithubIcon className="size-4" aria-hidden="true" />
64
  </a>
lightrag_webui/src/stores/settings.ts CHANGED
@@ -4,6 +4,7 @@ import { createSelectors } from '@/lib/utils'
4
  import { defaultQueryLabel } from '@/lib/constants'
5
 
6
  type Theme = 'dark' | 'light' | 'system'
 
7
 
8
  interface SettingsState {
9
  theme: Theme
@@ -27,6 +28,12 @@ interface SettingsState {
27
 
28
  apiKey: string | null
29
  setApiKey: (key: string | null) => void
 
 
 
 
 
 
30
  }
31
 
32
  const useSettingsStoreBase = create<SettingsState>()(
@@ -49,6 +56,10 @@ const useSettingsStoreBase = create<SettingsState>()(
49
 
50
  apiKey: null,
51
 
 
 
 
 
52
  setTheme: (theme: Theme) => set({ theme }),
53
 
54
  setQueryLabel: (queryLabel: string) =>
@@ -58,12 +69,19 @@ const useSettingsStoreBase = create<SettingsState>()(
58
 
59
  setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
60
 
61
- setApiKey: (apiKey: string | null) => set({ apiKey })
 
 
 
 
62
  }),
63
  {
64
  name: 'settings-storage',
65
  storage: createJSONStorage(() => localStorage),
66
- version: 4,
 
 
 
67
  migrate: (state: any, version: number) => {
68
  if (version < 2) {
69
  state.showEdgeLabel = false
@@ -78,6 +96,9 @@ const useSettingsStoreBase = create<SettingsState>()(
78
  state.enableHealthCheck = true
79
  state.apiKey = null
80
  }
 
 
 
81
  }
82
  }
83
  )
 
4
  import { defaultQueryLabel } from '@/lib/constants'
5
 
6
  type Theme = 'dark' | 'light' | 'system'
7
+ type Tab = 'documents' | 'knowledge-graph' | 'retrieval'
8
 
9
  interface SettingsState {
10
  theme: Theme
 
28
 
29
  apiKey: string | null
30
  setApiKey: (key: string | null) => void
31
+
32
+ currentTab: Tab
33
+ setCurrentTab: (tab: Tab) => void
34
+
35
+ retrievalHistory: any[]
36
+ setRetrievalHistory: (history: any[]) => void
37
  }
38
 
39
  const useSettingsStoreBase = create<SettingsState>()(
 
56
 
57
  apiKey: null,
58
 
59
+ currentTab: 'documents',
60
+
61
+ retrievalHistory: [],
62
+
63
  setTheme: (theme: Theme) => set({ theme }),
64
 
65
  setQueryLabel: (queryLabel: string) =>
 
69
 
70
  setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
71
 
72
+ setApiKey: (apiKey: string | null) => set({ apiKey }),
73
+
74
+ setCurrentTab: (tab: Tab) => set({ currentTab: tab }),
75
+
76
+ setRetrievalHistory: (history: any[]) => set({ retrievalHistory: history })
77
  }),
78
  {
79
  name: 'settings-storage',
80
  storage: createJSONStorage(() => localStorage),
81
+ version: 5,
82
+ partialize(state) {
83
+ return { ...state, retrievalHistory: undefined }
84
+ },
85
  migrate: (state: any, version: number) => {
86
  if (version < 2) {
87
  state.showEdgeLabel = false
 
96
  state.enableHealthCheck = true
97
  state.apiKey = null
98
  }
99
+ if (version < 5) {
100
+ state.currentTab = 'documents'
101
+ }
102
  }
103
  }
104
  )