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 +2 -3
- lightrag_webui/src/App.tsx +30 -16
- lightrag_webui/src/api/lightrag.ts +54 -21
- lightrag_webui/src/components/ThemeToggle.tsx +2 -0
- lightrag_webui/src/components/document/ClearDocumentsDialog.tsx +3 -3
- lightrag_webui/src/components/document/UploadDocumentsDialog.tsx +2 -2
- lightrag_webui/src/components/ui/Table.tsx +2 -0
- lightrag_webui/src/features/DocumentManager.tsx +31 -31
- lightrag_webui/src/features/RetrievalTesting.tsx +170 -0
- lightrag_webui/src/features/SiteHeader.tsx +28 -9
- lightrag_webui/src/stores/settings.ts +23 -2
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 |
-
//
|
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 |
-
<
|
36 |
-
<Tabs
|
|
|
|
|
|
|
|
|
37 |
<SiteHeader />
|
38 |
-
<
|
39 |
-
<
|
40 |
-
|
41 |
-
|
42 |
-
<
|
43 |
-
|
44 |
-
|
45 |
-
<
|
46 |
-
|
|
|
|
|
47 |
</Tabs>
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
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 (
|
155 |
-
|
156 |
-
|
157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
|
159 |
-
|
160 |
-
|
161 |
-
|
162 |
-
|
163 |
-
|
164 |
-
|
165 |
-
|
166 |
-
|
167 |
-
|
168 |
-
|
169 |
-
|
170 |
-
|
171 |
-
|
172 |
-
|
173 |
-
onChunk(data.response)
|
174 |
}
|
175 |
-
|
176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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=
|
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="
|
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 |
-
}, [
|
58 |
|
59 |
-
useEffect(() => {
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
69 |
-
|
70 |
-
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
75 |
-
|
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-
|
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=
|
|
|
|
|
|
|
|
|
|
|
22 |
>
|
23 |
Documents
|
24 |
</TabsTrigger>
|
25 |
<TabsTrigger
|
26 |
value="knowledge-graph"
|
27 |
-
className=
|
|
|
|
|
|
|
|
|
|
|
28 |
>
|
29 |
Knowledge Graph
|
30 |
</TabsTrigger>
|
31 |
-
|
32 |
-
value="
|
33 |
-
className=
|
|
|
|
|
|
|
|
|
|
|
34 |
>
|
35 |
-
|
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:
|
|
|
|
|
|
|
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 |
)
|