Added missing htmlFor attributes to labels and corresponding IDs to form elements throughout the web UI to enhance accessibility. This ensures screen readers can correctly identify form controls and improves browser autofill functionality. Changes include: - Fixed label associations in login form - Added proper IDs to form elements in Settings component - Replaced decorative labels with semantic headings in PropertiesView - Added screen reader accessible labels in RetrievalTesting - Improved checkbox accessibility in QuerySettings
173 lines
5.9 KiB
TypeScript
173 lines
5.9 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
|
|
})
|
|
}
|
|
|
|
// Prepare query parameters
|
|
const state = useSettingsStore.getState()
|
|
const queryParams = {
|
|
...state.querySettings,
|
|
query: userMessage.content,
|
|
conversation_history: prevMessages
|
|
.filter((m) => m.isError !== true)
|
|
.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>
|
|
)
|
|
}
|