enhance web UI with retrieval testing and UI improvements
This commit is contained in:
@@ -10,7 +10,7 @@ import react from 'eslint-plugin-react'
|
|||||||
export default tseslint.config(
|
export default tseslint.config(
|
||||||
{ ignores: ['dist'] },
|
{ ignores: ['dist'] },
|
||||||
{
|
{
|
||||||
extends: [js.configs.recommended, ...tseslint.configs.recommended],
|
extends: [js.configs.recommended, ...tseslint.configs.recommended, prettier],
|
||||||
files: ['**/*.{ts,tsx,js,jsx}'],
|
files: ['**/*.{ts,tsx,js,jsx}'],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
ecmaVersion: 2020,
|
ecmaVersion: 2020,
|
||||||
@@ -31,7 +31,6 @@ export default tseslint.config(
|
|||||||
'@stylistic/js/indent': ['error', 2],
|
'@stylistic/js/indent': ['error', 2],
|
||||||
'@stylistic/js/quotes': ['error', 'single'],
|
'@stylistic/js/quotes': ['error', 'single'],
|
||||||
'@typescript-eslint/no-explicit-any': ['off']
|
'@typescript-eslint/no-explicit-any': ['off']
|
||||||
},
|
}
|
||||||
prettier
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { useState, useCallback } from 'react'
|
||||||
import ThemeProvider from '@/components/ThemeProvider'
|
import ThemeProvider from '@/components/ThemeProvider'
|
||||||
import MessageAlert from '@/components/MessageAlert'
|
import MessageAlert from '@/components/MessageAlert'
|
||||||
import StatusIndicator from '@/components/StatusIndicator'
|
import StatusIndicator from '@/components/StatusIndicator'
|
||||||
@@ -10,14 +11,16 @@ import SiteHeader from '@/features/SiteHeader'
|
|||||||
|
|
||||||
import GraphViewer from '@/features/GraphViewer'
|
import GraphViewer from '@/features/GraphViewer'
|
||||||
import DocumentManager from '@/features/DocumentManager'
|
import DocumentManager from '@/features/DocumentManager'
|
||||||
|
import RetrievalTesting from '@/features/RetrievalTesting'
|
||||||
|
|
||||||
import { Tabs, TabsContent } from '@/components/ui/Tabs'
|
import { Tabs, TabsContent } from '@/components/ui/Tabs'
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const message = useBackendState.use.message()
|
const message = useBackendState.use.message()
|
||||||
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
const enableHealthCheck = useSettingsStore.use.enableHealthCheck()
|
||||||
|
const [currentTab] = useState(() => useSettingsStore.getState().currentTab)
|
||||||
|
|
||||||
// health check
|
// Health check
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableHealthCheck) return
|
if (!enableHealthCheck) return
|
||||||
|
|
||||||
@@ -30,25 +33,36 @@ function App() {
|
|||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [enableHealthCheck])
|
}, [enableHealthCheck])
|
||||||
|
|
||||||
|
const handleTabChange = useCallback(
|
||||||
|
(tab: string) => useSettingsStore.getState().setCurrentTab(tab as any),
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<div className="flex h-screen w-screen">
|
<main className="flex h-screen w-screen overflow-x-hidden">
|
||||||
<Tabs defaultValue="knowledge-graph" className="flex size-full flex-col">
|
<Tabs
|
||||||
|
defaultValue={currentTab}
|
||||||
|
className="!m-0 flex grow flex-col !p-0"
|
||||||
|
onValueChange={handleTabChange}
|
||||||
|
>
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<TabsContent value="documents" className="flex-1">
|
<div className="relative grow">
|
||||||
<DocumentManager />
|
<TabsContent value="documents" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
</TabsContent>
|
<DocumentManager />
|
||||||
<TabsContent value="knowledge-graph" className="flex-1">
|
</TabsContent>
|
||||||
<GraphViewer />
|
<TabsContent value="knowledge-graph" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
</TabsContent>
|
<GraphViewer />
|
||||||
<TabsContent value="settings" className="size-full">
|
</TabsContent>
|
||||||
<h1> Settings </h1>
|
<TabsContent value="retrieval" className="absolute top-0 right-0 bottom-0 left-0">
|
||||||
</TabsContent>
|
<RetrievalTesting />
|
||||||
|
</TabsContent>
|
||||||
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
{enableHealthCheck && <StatusIndicator />}
|
||||||
{enableHealthCheck && <StatusIndicator />}
|
{message !== null && <MessageAlert />}
|
||||||
{message !== null && <MessageAlert />}
|
<Toaster />
|
||||||
<Toaster />
|
</main>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@@ -151,32 +151,64 @@ export const queryText = async (request: QueryRequest): Promise<QueryResponse> =
|
|||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export const queryTextStream = async (request: QueryRequest, onChunk: (chunk: string) => void) => {
|
export const queryTextStream = async (
|
||||||
const response = await axiosInstance.post('/query/stream', request, {
|
request: QueryRequest,
|
||||||
responseType: 'stream'
|
onChunk: (chunk: string) => void,
|
||||||
})
|
onError?: (error: string) => void
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
let buffer = ''
|
||||||
|
await axiosInstance.post('/query/stream', request, {
|
||||||
|
responseType: 'text',
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/x-ndjson'
|
||||||
|
},
|
||||||
|
transformResponse: [
|
||||||
|
(data: string) => {
|
||||||
|
// Accumulate the data and process complete lines
|
||||||
|
buffer += data
|
||||||
|
const lines = buffer.split('\n')
|
||||||
|
// Keep the last potentially incomplete line in the buffer
|
||||||
|
buffer = lines.pop() || ''
|
||||||
|
|
||||||
const reader = response.data.getReader()
|
for (const line of lines) {
|
||||||
const decoder = new TextDecoder()
|
if (line.trim()) {
|
||||||
|
try {
|
||||||
while (true) {
|
const parsed = JSON.parse(line)
|
||||||
const { done, value } = await reader.read()
|
if (parsed.response) {
|
||||||
if (done) break
|
onChunk(parsed.response)
|
||||||
|
} else if (parsed.error && onError) {
|
||||||
const chunk = decoder.decode(value)
|
onError(parsed.error)
|
||||||
const lines = chunk.split('\n')
|
}
|
||||||
for (const line of lines) {
|
} catch (e) {
|
||||||
if (line) {
|
console.error('Error parsing stream chunk:', e)
|
||||||
try {
|
if (onError) onError('Error parsing server response')
|
||||||
const data = JSON.parse(line)
|
}
|
||||||
if (data.response) {
|
}
|
||||||
onChunk(data.response)
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
return data
|
||||||
console.error('Error parsing stream chunk:', e)
|
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Process any remaining data in the buffer
|
||||||
|
if (buffer.trim()) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(buffer)
|
||||||
|
if (parsed.response) {
|
||||||
|
onChunk(parsed.response)
|
||||||
|
} else if (parsed.error && onError) {
|
||||||
|
onError(parsed.error)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error parsing final chunk:', e)
|
||||||
|
if (onError) onError('Error parsing server response')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
const message = errorMessage(error)
|
||||||
|
console.error('Stream request failed:', message)
|
||||||
|
if (onError) onError(message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,6 +231,7 @@ export const uploadDocument = async (
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data'
|
'Content-Type': 'multipart/form-data'
|
||||||
},
|
},
|
||||||
|
// prettier-ignore
|
||||||
onUploadProgress:
|
onUploadProgress:
|
||||||
onUploadProgress !== undefined
|
onUploadProgress !== undefined
|
||||||
? (progressEvent) => {
|
? (progressEvent) => {
|
||||||
|
@@ -19,6 +19,7 @@ export default function ThemeToggle() {
|
|||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
tooltip="Switch to light theme"
|
tooltip="Switch to light theme"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
side="bottom"
|
||||||
>
|
>
|
||||||
<MoonIcon />
|
<MoonIcon />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -30,6 +31,7 @@ export default function ThemeToggle() {
|
|||||||
variant={controlButtonVariant}
|
variant={controlButtonVariant}
|
||||||
tooltip="Switch to dark theme"
|
tooltip="Switch to dark theme"
|
||||||
size="icon"
|
size="icon"
|
||||||
|
side="bottom"
|
||||||
>
|
>
|
||||||
<SunIcon />
|
<SunIcon />
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -15,7 +15,7 @@ import { clearDocuments } from '@/api/lightrag'
|
|||||||
import { EraserIcon } from 'lucide-react'
|
import { EraserIcon } from 'lucide-react'
|
||||||
|
|
||||||
export default function ClearDocumentsDialog() {
|
export default function ClearDocumentsDialog() {
|
||||||
const [open, setOpen] = useState(false) // 添加状态控制
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const handleClear = useCallback(async () => {
|
const handleClear = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -34,8 +34,8 @@ export default function ClearDocumentsDialog() {
|
|||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" tooltip="Clear documents" side="bottom" size="icon">
|
<Button variant="outline" side="bottom" tooltip='Clear documents' size="sm">
|
||||||
<EraserIcon />
|
<EraserIcon/> Clear
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
|
@@ -66,8 +66,8 @@ export default function UploadDocumentsDialog() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline" tooltip="Upload documents" side="bottom" size="icon">
|
<Button variant="default" side="bottom" tooltip='Upload documents' size="sm">
|
||||||
<UploadIcon />
|
<UploadIcon /> Upload
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
<DialogContent className="sm:max-w-xl" onCloseAutoFocus={(e) => e.preventDefault()}>
|
||||||
|
@@ -56,6 +56,7 @@ TableRow.displayName = 'TableRow'
|
|||||||
const TableHead = React.forwardRef<
|
const TableHead = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<th
|
<th
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@@ -71,6 +72,7 @@ TableHead.displayName = 'TableHead'
|
|||||||
const TableCell = React.forwardRef<
|
const TableCell = React.forwardRef<
|
||||||
HTMLTableCellElement,
|
HTMLTableCellElement,
|
||||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||||
|
// eslint-disable-next-line react/prop-types
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<td
|
<td
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
@@ -16,23 +16,23 @@ import ClearDocumentsDialog from '@/components/document/ClearDocumentsDialog'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
getDocuments,
|
getDocuments,
|
||||||
getDocumentsScanProgress,
|
// getDocumentsScanProgress,
|
||||||
scanNewDocuments,
|
scanNewDocuments
|
||||||
LightragDocumentsScanProgress
|
// LightragDocumentsScanProgress
|
||||||
} from '@/api/lightrag'
|
} from '@/api/lightrag'
|
||||||
import { errorMessage } from '@/lib/utils'
|
import { errorMessage } from '@/lib/utils'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
import { useBackendState } from '@/stores/state'
|
// import { useBackendState } from '@/stores/state'
|
||||||
|
|
||||||
import { RefreshCwIcon, TrashIcon } from 'lucide-react'
|
import { RefreshCwIcon, TrashIcon } from 'lucide-react'
|
||||||
|
|
||||||
// type DocumentStatus = 'indexed' | 'pending' | 'indexing' | 'error'
|
// type DocumentStatus = 'indexed' | 'pending' | 'indexing' | 'error'
|
||||||
|
|
||||||
export default function DocumentManager() {
|
export default function DocumentManager() {
|
||||||
const health = useBackendState.use.health()
|
// const health = useBackendState.use.health()
|
||||||
const [files, setFiles] = useState<string[]>([])
|
const [files, setFiles] = useState<string[]>([])
|
||||||
const [indexedFiles, setIndexedFiles] = useState<string[]>([])
|
const [indexedFiles, setIndexedFiles] = useState<string[]>([])
|
||||||
const [scanProgress, setScanProgress] = useState<LightragDocumentsScanProgress | null>(null)
|
// const [scanProgress, setScanProgress] = useState<LightragDocumentsScanProgress | null>(null)
|
||||||
|
|
||||||
const fetchDocuments = useCallback(async () => {
|
const fetchDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -45,7 +45,7 @@ export default function DocumentManager() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDocuments()
|
fetchDocuments()
|
||||||
}, [])
|
}, []) // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
const scanDocuments = useCallback(async () => {
|
const scanDocuments = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -54,26 +54,26 @@ export default function DocumentManager() {
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast.error('Failed to load documents\n' + errorMessage(err))
|
toast.error('Failed to load documents\n' + errorMessage(err))
|
||||||
}
|
}
|
||||||
}, [setFiles])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
// useEffect(() => {
|
||||||
const interval = setInterval(async () => {
|
// const interval = setInterval(async () => {
|
||||||
try {
|
// try {
|
||||||
if (!health) return
|
// if (!health) return
|
||||||
const progress = await getDocumentsScanProgress()
|
// const progress = await getDocumentsScanProgress()
|
||||||
setScanProgress((pre) => {
|
// setScanProgress((pre) => {
|
||||||
if (pre?.is_scanning === progress.is_scanning && progress.is_scanning === false) {
|
// if (pre?.is_scanning === progress.is_scanning && progress.is_scanning === false) {
|
||||||
return pre
|
// return pre
|
||||||
}
|
// }
|
||||||
return progress
|
// return progress
|
||||||
})
|
// })
|
||||||
console.log(progress)
|
// console.log(progress)
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
toast.error('Failed to get scan progress\n' + errorMessage(err))
|
// toast.error('Failed to get scan progress\n' + errorMessage(err))
|
||||||
}
|
// }
|
||||||
}, 2000)
|
// }, 2000)
|
||||||
return () => clearInterval(interval)
|
// return () => clearInterval(interval)
|
||||||
}, [health])
|
// }, [health])
|
||||||
|
|
||||||
const handleDelete = async (fileName: string) => {
|
const handleDelete = async (fileName: string) => {
|
||||||
console.log(`deleting ${fileName}`)
|
console.log(`deleting ${fileName}`)
|
||||||
@@ -88,19 +88,19 @@ export default function DocumentManager() {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
|
||||||
tooltip="Scan Documents"
|
|
||||||
onClick={scanDocuments}
|
onClick={scanDocuments}
|
||||||
side="bottom"
|
side="bottom"
|
||||||
|
tooltip="Scan documents"
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<RefreshCwIcon />
|
<RefreshCwIcon /> Scan
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex-1" />
|
<div className="flex-1" />
|
||||||
<ClearDocumentsDialog />
|
<ClearDocumentsDialog />
|
||||||
<UploadDocumentsDialog />
|
<UploadDocumentsDialog />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{scanProgress?.is_scanning && (
|
{/* {scanProgress?.is_scanning && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between text-sm">
|
<div className="flex justify-between text-sm">
|
||||||
<span>Indexing {scanProgress.current_file}</span>
|
<span>Indexing {scanProgress.current_file}</span>
|
||||||
@@ -108,7 +108,7 @@ export default function DocumentManager() {
|
|||||||
</div>
|
</div>
|
||||||
<Progress value={scanProgress.progress} />
|
<Progress value={scanProgress.progress} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)} */}
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
170
lightrag_webui/src/features/RetrievalTesting.tsx
Normal file
170
lightrag_webui/src/features/RetrievalTesting.tsx
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import Input from '@/components/ui/Input'
|
||||||
|
import Button from '@/components/ui/Button'
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import { queryTextStream, QueryMode } from '@/api/lightrag'
|
||||||
|
import { errorMessage } from '@/lib/utils'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useDebounce } from '@/hooks/useDebounce'
|
||||||
|
import { EraserIcon, SendIcon, LoaderIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
type Message = {
|
||||||
|
id: string
|
||||||
|
content: string
|
||||||
|
role: 'User' | 'LightRAG'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RetrievalTesting() {
|
||||||
|
const [messages, setMessages] = useState<Message[]>(
|
||||||
|
() => useSettingsStore.getState().retrievalHistory || []
|
||||||
|
)
|
||||||
|
const [inputValue, setInputValue] = useState('')
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [mode, setMode] = useState<QueryMode>('mix')
|
||||||
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const scrollToBottom = useCallback(() => {
|
||||||
|
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleSubmit = useCallback(
|
||||||
|
async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!inputValue.trim() || isLoading) return
|
||||||
|
|
||||||
|
const userMessage: Message = {
|
||||||
|
id: Date.now().toString(),
|
||||||
|
content: inputValue,
|
||||||
|
role: 'User'
|
||||||
|
}
|
||||||
|
|
||||||
|
const assistantMessage: Message = {
|
||||||
|
id: (Date.now() + 1).toString(),
|
||||||
|
content: '',
|
||||||
|
role: 'LightRAG'
|
||||||
|
}
|
||||||
|
|
||||||
|
setMessages((prev) => {
|
||||||
|
const newMessages = [...prev, userMessage, assistantMessage]
|
||||||
|
return newMessages
|
||||||
|
})
|
||||||
|
|
||||||
|
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 === 'LightRAG') {
|
||||||
|
lastMessage.content = assistantMessage.content
|
||||||
|
}
|
||||||
|
return newMessages
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await queryTextStream(
|
||||||
|
{
|
||||||
|
query: userMessage.content,
|
||||||
|
mode: mode,
|
||||||
|
stream: true
|
||||||
|
},
|
||||||
|
updateAssistantMessage
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
updateAssistantMessage(`Error: Failed to get response\n${errorMessage(err)}`)
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false)
|
||||||
|
useSettingsStore
|
||||||
|
.getState()
|
||||||
|
.setRetrievalHistory([
|
||||||
|
...useSettingsStore.getState().retrievalHistory,
|
||||||
|
userMessage,
|
||||||
|
assistantMessage
|
||||||
|
])
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[inputValue, isLoading, mode, setMessages]
|
||||||
|
)
|
||||||
|
|
||||||
|
const debouncedMessages = useDebounce(messages, 100)
|
||||||
|
useEffect(() => scrollToBottom(), [debouncedMessages, scrollToBottom])
|
||||||
|
|
||||||
|
const clearMessages = useCallback(() => {
|
||||||
|
setMessages([])
|
||||||
|
useSettingsStore.getState().setRetrievalHistory([])
|
||||||
|
}, [setMessages])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full flex-col gap-4 px-32 py-6">
|
||||||
|
<div className="relative grow">
|
||||||
|
<div className="bg-primary-foreground/60 absolute inset-0 flex flex-col overflow-auto rounded-lg border p-2">
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col gap-2">
|
||||||
|
{messages.length === 0 ? (
|
||||||
|
<div className="text-muted-foreground flex h-full items-center justify-center text-lg">
|
||||||
|
Start a retrieval by typing your query below
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
messages.map((message) => (
|
||||||
|
<div
|
||||||
|
key={message.id}
|
||||||
|
className={`flex ${message.role === 'User' ? 'justify-end' : 'justify-start'}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`max-w-[80%] rounded-lg px-4 py-2 ${
|
||||||
|
message.role === 'User' ? 'bg-primary text-primary-foreground' : 'bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<pre className="break-words whitespace-pre-wrap">{message.content}</pre>
|
||||||
|
{message.content.length === 0 && (
|
||||||
|
<LoaderIcon className="animate-spin duration-2000" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
<div ref={messagesEndRef} className="pb-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2 pb-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={clearMessages}
|
||||||
|
disabled={isLoading}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
<EraserIcon />
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
|
<select
|
||||||
|
className="border-input bg-background ring-offset-background h-9 rounded-md border px-3 py-1 text-sm"
|
||||||
|
value={mode}
|
||||||
|
onChange={(e) => setMode(e.target.value as QueryMode)}
|
||||||
|
disabled={isLoading}
|
||||||
|
>
|
||||||
|
<option value="naive">Naive</option>
|
||||||
|
<option value="local">Local</option>
|
||||||
|
<option value="global">Global</option>
|
||||||
|
<option value="hybrid">Hybrid</option>
|
||||||
|
<option value="mix">Mix</option>
|
||||||
|
</select>
|
||||||
|
<Input
|
||||||
|
className="flex-1"
|
||||||
|
value={inputValue}
|
||||||
|
onChange={(e) => setInputValue(e.target.value)}
|
||||||
|
placeholder="Type your query..."
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<Button type="submit" variant="default" disabled={isLoading} size="sm">
|
||||||
|
<SendIcon />
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@@ -2,14 +2,18 @@ import Button from '@/components/ui/Button'
|
|||||||
import { SiteInfo } from '@/lib/constants'
|
import { SiteInfo } from '@/lib/constants'
|
||||||
import ThemeToggle from '@/components/ThemeToggle'
|
import ThemeToggle from '@/components/ThemeToggle'
|
||||||
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
import { TabsList, TabsTrigger } from '@/components/ui/Tabs'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
import { ZapIcon, GithubIcon } from 'lucide-react'
|
import { ZapIcon, GithubIcon } from 'lucide-react'
|
||||||
|
|
||||||
export default function SiteHeader() {
|
export default function SiteHeader() {
|
||||||
|
const currentTab = useSettingsStore.use.currentTab()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<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">
|
<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">
|
||||||
<a href="/" className="mr-6 flex items-center gap-2">
|
<a href="/" className="mr-6 flex items-center gap-2">
|
||||||
<ZapIcon className="size-4 text-teal-400" aria-hidden="true" />
|
<ZapIcon className="size-4 text-emerald-400" aria-hidden="true" />
|
||||||
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
|
<span className="font-bold md:inline-block">{SiteInfo.name}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -18,28 +22,43 @@ export default function SiteHeader() {
|
|||||||
<TabsList className="h-full gap-2">
|
<TabsList className="h-full gap-2">
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="documents"
|
value="documents"
|
||||||
className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
|
className={cn(
|
||||||
|
'cursor-pointer px-2 py-1 transition-all',
|
||||||
|
currentTab === 'documents'
|
||||||
|
? '!bg-emerald-400 !text-zinc-50'
|
||||||
|
: 'hover:bg-background/60'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Documents
|
Documents
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="knowledge-graph"
|
value="knowledge-graph"
|
||||||
className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
|
className={cn(
|
||||||
|
'cursor-pointer px-2 py-1 transition-all',
|
||||||
|
currentTab === 'knowledge-graph'
|
||||||
|
? '!bg-emerald-400 !text-zinc-50'
|
||||||
|
: 'hover:bg-background/60'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Knowledge Graph
|
Knowledge Graph
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
{/* <TabsTrigger
|
<TabsTrigger
|
||||||
value="settings"
|
value="retrieval"
|
||||||
className="hover:bg-background/60 cursor-pointer px-2 py-1 transition-all"
|
className={cn(
|
||||||
|
'cursor-pointer px-2 py-1 transition-all',
|
||||||
|
currentTab === 'retrieval'
|
||||||
|
? '!bg-emerald-400 !text-zinc-50'
|
||||||
|
: 'hover:bg-background/60'
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
Settings
|
Retrieval
|
||||||
</TabsTrigger> */}
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav className="flex items-center">
|
<nav className="flex items-center">
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon" side="bottom" tooltip="Project Repository">
|
||||||
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
<a href={SiteInfo.github} target="_blank" rel="noopener noreferrer">
|
||||||
<GithubIcon className="size-4" aria-hidden="true" />
|
<GithubIcon className="size-4" aria-hidden="true" />
|
||||||
</a>
|
</a>
|
||||||
|
@@ -4,6 +4,7 @@ import { createSelectors } from '@/lib/utils'
|
|||||||
import { defaultQueryLabel } from '@/lib/constants'
|
import { defaultQueryLabel } from '@/lib/constants'
|
||||||
|
|
||||||
type Theme = 'dark' | 'light' | 'system'
|
type Theme = 'dark' | 'light' | 'system'
|
||||||
|
type Tab = 'documents' | 'knowledge-graph' | 'retrieval'
|
||||||
|
|
||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
theme: Theme
|
theme: Theme
|
||||||
@@ -27,6 +28,12 @@ interface SettingsState {
|
|||||||
|
|
||||||
apiKey: string | null
|
apiKey: string | null
|
||||||
setApiKey: (key: string | null) => void
|
setApiKey: (key: string | null) => void
|
||||||
|
|
||||||
|
currentTab: Tab
|
||||||
|
setCurrentTab: (tab: Tab) => void
|
||||||
|
|
||||||
|
retrievalHistory: any[]
|
||||||
|
setRetrievalHistory: (history: any[]) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const useSettingsStoreBase = create<SettingsState>()(
|
const useSettingsStoreBase = create<SettingsState>()(
|
||||||
@@ -49,6 +56,10 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
|
|
||||||
apiKey: null,
|
apiKey: null,
|
||||||
|
|
||||||
|
currentTab: 'documents',
|
||||||
|
|
||||||
|
retrievalHistory: [],
|
||||||
|
|
||||||
setTheme: (theme: Theme) => set({ theme }),
|
setTheme: (theme: Theme) => set({ theme }),
|
||||||
|
|
||||||
setQueryLabel: (queryLabel: string) =>
|
setQueryLabel: (queryLabel: string) =>
|
||||||
@@ -58,12 +69,19 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
|
|
||||||
setEnableHealthCheck: (enable: boolean) => set({ enableHealthCheck: enable }),
|
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: any[]) => set({ retrievalHistory: history })
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'settings-storage',
|
name: 'settings-storage',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => localStorage),
|
||||||
version: 4,
|
version: 5,
|
||||||
|
partialize(state) {
|
||||||
|
return { ...state, retrievalHistory: undefined }
|
||||||
|
},
|
||||||
migrate: (state: any, version: number) => {
|
migrate: (state: any, version: number) => {
|
||||||
if (version < 2) {
|
if (version < 2) {
|
||||||
state.showEdgeLabel = false
|
state.showEdgeLabel = false
|
||||||
@@ -78,6 +96,9 @@ const useSettingsStoreBase = create<SettingsState>()(
|
|||||||
state.enableHealthCheck = true
|
state.enableHealthCheck = true
|
||||||
state.apiKey = null
|
state.apiKey = null
|
||||||
}
|
}
|
||||||
|
if (version < 5) {
|
||||||
|
state.currentTab = 'documents'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user