Files
lightrag/lightrag_webui/src/features/RetrievalTesting.tsx
yangdx 46ffb6afa4 fix: improve form accessibility with proper label associations
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
2025-04-07 05:20:12 +08:00

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>
)
}