Files
lightrag/lightrag_webui/src/features/RetrievalTesting.tsx
2025-04-11 00:45:37 +08:00

175 lines
6.0 KiB
TypeScript

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 { ChatMessage, MessageWithError } from '@/components/retrieval/ChatMessage'
import { EraserIcon, SendIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function RetrievalTesting() {
const { t } = useTranslation()
const [messages, setMessages] = useState<MessageWithError[]>(
() => useSettingsStore.getState().retrievalHistory || []
)
const [inputValue, setInputValue] = useState('')
const [isLoading, setIsLoading] = useState(false)
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
// 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, isError?: boolean) => {
assistantMessage.content += chunk
setMessages((prev) => {
const newMessages = [...prev]
const lastMessage = newMessages[newMessages.length - 1]
if (lastMessage.role === 'assistant') {
lastMessage.content = assistantMessage.content
lastMessage.isError = isError
}
return newMessages
})
scrollToBottom()
}
// Prepare query parameters
const state = useSettingsStore.getState()
const queryParams = {
...state.querySettings,
query: userMessage.content,
conversation_history: prevMessages
.filter((m) => m.isError !== true)
.slice(-(state.querySettings.history_turns || 0) * 2)
.map((m) => ({ role: m.role, content: m.content }))
}
try {
// Run query
if (state.querySettings.stream) {
let errorMessage = ''
await queryTextStream(queryParams, updateAssistantMessage, (error) => {
errorMessage += error
})
if (errorMessage) {
if (assistantMessage.content) {
errorMessage = assistantMessage.content + '\n' + errorMessage
}
updateAssistantMessage(errorMessage, true)
}
} else {
const response = await queryText(queryParams)
updateAssistantMessage(response.response)
}
} catch (err) {
// Handle error
updateAssistantMessage(`${t('retrievePanel.retrieval.error')}\n${errorMessage(err)}`, true)
} finally {
// Clear loading and add messages to state
setIsLoading(false)
useSettingsStore
.getState()
.setRetrievalHistory([...prevMessages, userMessage, assistantMessage])
}
},
[inputValue, isLoading, messages, setMessages, t]
)
const debouncedMessages = useDebounce(messages, 100)
useEffect(() => scrollToBottom(), [debouncedMessages, scrollToBottom])
const clearMessages = useCallback(() => {
setMessages([])
useSettingsStore.getState().setRetrievalHistory([])
}, [setMessages])
return (
<div className="flex size-full gap-2 px-2 pb-12 overflow-hidden">
<div className="flex grow flex-col gap-4">
<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">
{t('retrievePanel.retrieval.startPrompt')}
</div>
) : (
messages.map((message, idx) => (
<div
key={idx}
className={`flex ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}
>
{<ChatMessage message={message} />}
</div>
))
)}
<div ref={messagesEndRef} className="pb-1" />
</div>
</div>
</div>
<form onSubmit={handleSubmit} className="flex shrink-0 items-center gap-2">
<Button
type="button"
variant="outline"
onClick={clearMessages}
disabled={isLoading}
size="sm"
>
<EraserIcon />
{t('retrievePanel.retrieval.clear')}
</Button>
<div className="flex-1 relative">
<label htmlFor="query-input" className="sr-only">
{t('retrievePanel.retrieval.placeholder')}
</label>
<Input
id="query-input"
className="w-full"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder={t('retrievePanel.retrieval.placeholder')}
disabled={isLoading}
/>
</div>
<Button type="submit" variant="default" disabled={isLoading} size="sm">
<SendIcon />
{t('retrievePanel.retrieval.send')}
</Button>
</form>
</div>
<QuerySettings />
</div>
)
}