Merge pull request #1291 from danielaskdd/main

fix: improve form accessibility with proper label associations
This commit is contained in:
Daniel.y
2025-04-07 05:27:07 +08:00
committed by GitHub
11 changed files with 257 additions and 197 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -8,8 +8,8 @@
<link rel="icon" type="image/svg+xml" href="logo.png" /> <link rel="icon" type="image/svg+xml" href="logo.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lightrag</title> <title>Lightrag</title>
<script type="module" crossorigin src="/webui/assets/index-DXSe8IZZ.js"></script> <script type="module" crossorigin src="/webui/assets/index-Bz5MOBb9.js"></script>
<link rel="stylesheet" crossorigin href="/webui/assets/index-QU59h9JG.css"> <link rel="stylesheet" crossorigin href="/webui/assets/index-CQhBIpFe.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -184,9 +184,11 @@ const PropertyRow = ({
return translation === translationKey ? name : translation return translation === translationKey ? name : translation
} }
// Since Text component uses a label internally, we'll use a span here instead of a label
// to avoid nesting labels which is not recommended for accessibility
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<label className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</label>: <span className="text-primary/60 tracking-wide whitespace-nowrap">{getPropertyNameTranslation(name)}</span>:
<Text <Text
className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis" className="hover:bg-primary/20 rounded p-1 overflow-hidden text-ellipsis"
tooltipClassName="max-w-80" tooltipClassName="max-w-80"
@@ -213,7 +215,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<label className="text-md pl-1 font-bold tracking-wide text-blue-700">{t('graphPanel.propertiesView.node.title')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-blue-700">{t('graphPanel.propertiesView.node.title')}</h3>
<div className="flex gap-3"> <div className="flex gap-3">
<Button <Button
size="icon" size="icon"
@@ -246,7 +248,7 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
/> />
<PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} /> <PropertyRow name={t('graphPanel.propertiesView.node.degree')} value={node.degree} />
</div> </div>
<label className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.node.properties')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.node.properties')}</h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(node.properties) {Object.keys(node.properties)
.sort() .sort()
@@ -256,9 +258,9 @@ const NodePropertiesView = ({ node }: { node: NodeType }) => {
</div> </div>
{node.relationships.length > 0 && ( {node.relationships.length > 0 && (
<> <>
<label className="text-md pl-1 font-bold tracking-wide text-emerald-700"> <h3 className="text-md pl-1 font-bold tracking-wide text-emerald-700">
{t('graphPanel.propertiesView.node.relationships')} {t('graphPanel.propertiesView.node.relationships')}
</label> </h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{node.relationships.map(({ type, id, label }) => { {node.relationships.map(({ type, id, label }) => {
return ( return (
@@ -283,7 +285,7 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-md pl-1 font-bold tracking-wide text-violet-700">{t('graphPanel.propertiesView.edge.title')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-violet-700">{t('graphPanel.propertiesView.edge.title')}</h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
<PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} /> <PropertyRow name={t('graphPanel.propertiesView.edge.id')} value={edge.id} />
{edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />} {edge.type && <PropertyRow name={t('graphPanel.propertiesView.edge.type')} value={edge.type} />}
@@ -302,7 +304,7 @@ const EdgePropertiesView = ({ edge }: { edge: EdgeType }) => {
}} }}
/> />
</div> </div>
<label className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.edge.properties')}</label> <h3 className="text-md pl-1 font-bold tracking-wide text-amber-700">{t('graphPanel.propertiesView.edge.properties')}</h3>
<div className="bg-primary/5 max-h-96 overflow-auto rounded p-1"> <div className="bg-primary/5 max-h-96 overflow-auto rounded p-1">
{Object.keys(edge.properties) {Object.keys(edge.properties)
.sort() .sort()

View File

@@ -23,11 +23,14 @@ const LabeledCheckBox = ({
onCheckedChange: () => void onCheckedChange: () => void
label: string label: string
}) => { }) => {
// Create unique ID using the label text converted to lowercase with spaces removed
const id = `checkbox-${label.toLowerCase().replace(/\s+/g, '-')}`;
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Checkbox checked={checked} onCheckedChange={onCheckedChange} /> <Checkbox id={id} checked={checked} onCheckedChange={onCheckedChange} />
<label <label
htmlFor="terms" htmlFor={id}
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
{label} {label}
@@ -56,6 +59,8 @@ const LabeledNumberInput = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [currentValue, setCurrentValue] = useState<number | null>(value) const [currentValue, setCurrentValue] = useState<number | null>(value)
// Create unique ID using the label text converted to lowercase with spaces removed
const id = `input-${label.toLowerCase().replace(/\s+/g, '-')}`;
const onValueChange = useCallback( const onValueChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => { (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -94,13 +99,14 @@ const LabeledNumberInput = ({
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label <label
htmlFor="terms" htmlFor={id}
className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70" className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
> >
{label} {label}
</label> </label>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Input <Input
id={id}
type="number" type="number"
value={currentValue === null ? '' : currentValue} value={currentValue === null ? '' : currentValue}
onChange={onValueChange} onChange={onValueChange}
@@ -295,11 +301,12 @@ export default function Settings() {
/> />
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<label className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70"> <label htmlFor="edge-size-min" className="text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
{t('graphPanel.sideBar.settings.edgeSizeRange')} {t('graphPanel.sideBar.settings.edgeSizeRange')}
</label> </label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Input <Input
id="edge-size-min"
type="number" type="number"
value={minEdgeSize} value={minEdgeSize}
onChange={(e) => { onChange={(e) => {
@@ -315,6 +322,7 @@ export default function Settings() {
<span>-</span> <span>-</span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Input <Input
id="edge-size-max"
type="number" type="number"
value={maxEdgeSize} value={maxEdgeSize}
onChange={(e) => { onChange={(e) => {

View File

@@ -93,14 +93,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.topKTooltip')} tooltip={t('retrievePanel.querySettings.topKTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="top_k" <label htmlFor="top_k" className="sr-only">
stepper={1} {t('retrievePanel.querySettings.topK')}
value={querySettings.top_k} </label>
onValueChange={(v) => handleChange('top_k', v)} <NumberInput
min={1} id="top_k"
placeholder={t('retrievePanel.querySettings.topKPlaceholder')} stepper={1}
/> value={querySettings.top_k}
onValueChange={(v) => handleChange('top_k', v)}
min={1}
placeholder={t('retrievePanel.querySettings.topKPlaceholder')}
/>
</div>
</> </>
{/* Max Tokens */} {/* Max Tokens */}
@@ -112,14 +117,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')} tooltip={t('retrievePanel.querySettings.maxTokensTextUnitTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="max_token_for_text_unit" <label htmlFor="max_token_for_text_unit" className="sr-only">
stepper={500} {t('retrievePanel.querySettings.maxTokensTextUnit')}
value={querySettings.max_token_for_text_unit} </label>
onValueChange={(v) => handleChange('max_token_for_text_unit', v)} <NumberInput
min={1} id="max_token_for_text_unit"
placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')} stepper={500}
/> value={querySettings.max_token_for_text_unit}
onValueChange={(v) => handleChange('max_token_for_text_unit', v)}
min={1}
placeholder={t('retrievePanel.querySettings.maxTokensTextUnit')}
/>
</div>
</> </>
<> <>
@@ -128,14 +138,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')} tooltip={t('retrievePanel.querySettings.maxTokensGlobalContextTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="max_token_for_global_context" <label htmlFor="max_token_for_global_context" className="sr-only">
stepper={500} {t('retrievePanel.querySettings.maxTokensGlobalContext')}
value={querySettings.max_token_for_global_context} </label>
onValueChange={(v) => handleChange('max_token_for_global_context', v)} <NumberInput
min={1} id="max_token_for_global_context"
placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')} stepper={500}
/> value={querySettings.max_token_for_global_context}
onValueChange={(v) => handleChange('max_token_for_global_context', v)}
min={1}
placeholder={t('retrievePanel.querySettings.maxTokensGlobalContext')}
/>
</div>
</> </>
<> <>
@@ -145,14 +160,19 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')} tooltip={t('retrievePanel.querySettings.maxTokensLocalContextTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
id="max_token_for_local_context" <label htmlFor="max_token_for_local_context" className="sr-only">
stepper={500} {t('retrievePanel.querySettings.maxTokensLocalContext')}
value={querySettings.max_token_for_local_context} </label>
onValueChange={(v) => handleChange('max_token_for_local_context', v)} <NumberInput
min={1} id="max_token_for_local_context"
placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')} stepper={500}
/> value={querySettings.max_token_for_local_context}
onValueChange={(v) => handleChange('max_token_for_local_context', v)}
min={1}
placeholder={t('retrievePanel.querySettings.maxTokensLocalContext')}
/>
</div>
</> </>
</> </>
@@ -164,16 +184,21 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')} tooltip={t('retrievePanel.querySettings.historyTurnsTooltip')}
side="left" side="left"
/> />
<NumberInput <div>
className="!border-input" <label htmlFor="history_turns" className="sr-only">
id="history_turns" {t('retrievePanel.querySettings.historyTurns')}
stepper={1} </label>
type="text" <NumberInput
value={querySettings.history_turns} className="!border-input"
onValueChange={(v) => handleChange('history_turns', v)} id="history_turns"
min={0} stepper={1}
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')} type="text"
/> value={querySettings.history_turns}
onValueChange={(v) => handleChange('history_turns', v)}
min={0}
placeholder={t('retrievePanel.querySettings.historyTurnsPlaceholder')}
/>
</div>
</> </>
{/* Keywords */} {/* Keywords */}
@@ -185,19 +210,24 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')} tooltip={t('retrievePanel.querySettings.hlKeywordsTooltip')}
side="left" side="left"
/> />
<Input <div>
id="hl_keywords" <label htmlFor="hl_keywords" className="sr-only">
type="text" {t('retrievePanel.querySettings.hlKeywords')}
value={querySettings.hl_keywords?.join(', ')} </label>
onChange={(e) => { <Input
const keywords = e.target.value id="hl_keywords"
.split(',') type="text"
.map((k) => k.trim()) value={querySettings.hl_keywords?.join(', ')}
.filter((k) => k !== '') onChange={(e) => {
handleChange('hl_keywords', keywords) const keywords = e.target.value
}} .split(',')
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')} .map((k) => k.trim())
/> .filter((k) => k !== '')
handleChange('hl_keywords', keywords)
}}
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
/>
</div>
</> </>
<> <>
@@ -207,32 +237,38 @@ export default function QuerySettings() {
tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')} tooltip={t('retrievePanel.querySettings.llKeywordsTooltip')}
side="left" side="left"
/> />
<Input <div>
id="ll_keywords" <label htmlFor="ll_keywords" className="sr-only">
type="text" {t('retrievePanel.querySettings.llKeywords')}
value={querySettings.ll_keywords?.join(', ')} </label>
onChange={(e) => { <Input
const keywords = e.target.value id="ll_keywords"
.split(',') type="text"
.map((k) => k.trim()) value={querySettings.ll_keywords?.join(', ')}
.filter((k) => k !== '') onChange={(e) => {
handleChange('ll_keywords', keywords) const keywords = e.target.value
}} .split(',')
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')} .map((k) => k.trim())
/> .filter((k) => k !== '')
handleChange('ll_keywords', keywords)
}}
placeholder={t('retrievePanel.querySettings.hlkeywordsPlaceHolder')}
/>
</div>
</> </>
</> </>
{/* Toggle Options */} {/* Toggle Options */}
<> <>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <label htmlFor="only_need_context" className="flex-1">
className="ml-1" <Text
text={t('retrievePanel.querySettings.onlyNeedContext')} className="ml-1"
tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')} text={t('retrievePanel.querySettings.onlyNeedContext')}
side="left" tooltip={t('retrievePanel.querySettings.onlyNeedContextTooltip')}
/> side="left"
<div className="grow" /> />
</label>
<Checkbox <Checkbox
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
id="only_need_context" id="only_need_context"
@@ -242,13 +278,14 @@ export default function QuerySettings() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <label htmlFor="only_need_prompt" className="flex-1">
className="ml-1" <Text
text={t('retrievePanel.querySettings.onlyNeedPrompt')} className="ml-1"
tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')} text={t('retrievePanel.querySettings.onlyNeedPrompt')}
side="left" tooltip={t('retrievePanel.querySettings.onlyNeedPromptTooltip')}
/> side="left"
<div className="grow" /> />
</label>
<Checkbox <Checkbox
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
id="only_need_prompt" id="only_need_prompt"
@@ -258,13 +295,14 @@ export default function QuerySettings() {
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Text <label htmlFor="stream" className="flex-1">
className="ml-1" <Text
text={t('retrievePanel.querySettings.streamResponse')} className="ml-1"
tooltip={t('retrievePanel.querySettings.streamResponseTooltip')} text={t('retrievePanel.querySettings.streamResponse')}
side="left" tooltip={t('retrievePanel.querySettings.streamResponseTooltip')}
/> side="left"
<div className="grow" /> />
</label>
<Checkbox <Checkbox
className="mr-1 cursor-pointer" className="mr-1 cursor-pointer"
id="stream" id="stream"

View File

@@ -507,8 +507,14 @@ export default function DocumentManager() {
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-gray-500">{t('documentPanel.documentManager.fileNameLabel')}</span> <label
htmlFor="toggle-filename-btn"
className="text-sm text-gray-500"
>
{t('documentPanel.documentManager.fileNameLabel')}
</label>
<Button <Button
id="toggle-filename-btn"
variant="outline" variant="outline"
size="sm" size="sm"
onClick={() => setShowFileName(!showFileName)} onClick={() => setShowFileName(!showFileName)}

View File

@@ -148,11 +148,11 @@ const LoginPage = () => {
<CardContent className="px-8 pb-8"> <CardContent className="px-8 pb-8">
<form onSubmit={handleSubmit} className="space-y-6"> <form onSubmit={handleSubmit} className="space-y-6">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<label htmlFor="username" className="text-sm font-medium w-16 shrink-0"> <label htmlFor="username-input" className="text-sm font-medium w-16 shrink-0">
{t('login.username')} {t('login.username')}
</label> </label>
<Input <Input
id="username" id="username-input"
placeholder={t('login.usernamePlaceholder')} placeholder={t('login.usernamePlaceholder')}
value={username} value={username}
onChange={(e) => setUsername(e.target.value)} onChange={(e) => setUsername(e.target.value)}
@@ -161,11 +161,11 @@ const LoginPage = () => {
/> />
</div> </div>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<label htmlFor="password" className="text-sm font-medium w-16 shrink-0"> <label htmlFor="password-input" className="text-sm font-medium w-16 shrink-0">
{t('login.password')} {t('login.password')}
</label> </label>
<Input <Input
id="password" id="password-input"
type="password" type="password"
placeholder={t('login.passwordPlaceholder')} placeholder={t('login.passwordPlaceholder')}
value={password} value={password}

View File

@@ -147,13 +147,19 @@ export default function RetrievalTesting() {
<EraserIcon /> <EraserIcon />
{t('retrievePanel.retrieval.clear')} {t('retrievePanel.retrieval.clear')}
</Button> </Button>
<Input <div className="flex-1 relative">
className="flex-1" <label htmlFor="query-input" className="sr-only">
value={inputValue} {t('retrievePanel.retrieval.placeholder')}
onChange={(e) => setInputValue(e.target.value)} </label>
placeholder={t('retrievePanel.retrieval.placeholder')} <Input
disabled={isLoading} 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"> <Button type="submit" variant="default" disabled={isLoading} size="sm">
<SendIcon /> <SendIcon />
{t('retrievePanel.retrieval.send')} {t('retrievePanel.retrieval.send')}

View File

@@ -296,11 +296,11 @@
"queryMode": "查询模式", "queryMode": "查询模式",
"queryModeTooltip": "选择检索策略:\n• Naive基础搜索无高级技术\n• Local上下文相关信息检索\n• Global利用全局知识库\n• Hybrid结合本地和全局检索\n• Mix整合知识图谱和向量检索", "queryModeTooltip": "选择检索策略:\n• Naive基础搜索无高级技术\n• Local上下文相关信息检索\n• Global利用全局知识库\n• Hybrid结合本地和全局检索\n• Mix整合知识图谱和向量检索",
"queryModeOptions": { "queryModeOptions": {
"naive": "朴素", "naive": "Naive",
"local": "本地", "local": "Local",
"global": "全局", "global": "Global",
"hybrid": "混合", "hybrid": "Hybrid",
"mix": "混合" "mix": "Mix"
}, },
"responseFormat": "响应格式", "responseFormat": "响应格式",
"responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点", "responseFormatTooltip": "定义响应格式。例如:\n• 多段落\n• 单段落\n• 要点",