diff --git a/frontend/src/locale/en.json b/frontend/src/locale/en.json index 0f02d7bd..fb315b62 100644 --- a/frontend/src/locale/en.json +++ b/frontend/src/locale/en.json @@ -137,6 +137,7 @@ "retrieval": { "title": "Retrieval", "note": "These changes take effect immediately, no re-ingest needed.", + "tag": "live · no re-ingest", "chunks": "Chunks to retrieve (top-k)", "scoreThreshold": "Score threshold", "scoreThresholdPlaceholder": "None", @@ -165,6 +166,7 @@ "chunking": { "title": "Chunking", "note": "Ingest-time settings. Changing these requires re-ingesting the source.", + "tag": "re-ingest to apply", "strategy": "Chunking strategy", "strategies": { "classic_chunk": "Classic", diff --git a/frontend/src/settings/components/RetrievalOptions.tsx b/frontend/src/settings/components/RetrievalOptions.tsx index d7baebe3..b20ce13a 100644 --- a/frontend/src/settings/components/RetrievalOptions.tsx +++ b/frontend/src/settings/components/RetrievalOptions.tsx @@ -1,6 +1,8 @@ -import { useMemo, useState } from 'react'; +import { type ReactNode, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; +import { cn } from '@/lib/utils'; + import { Button } from '../../components/ui/button'; import { Input } from '../../components/ui/input'; import { Label } from '../../components/ui/label'; @@ -83,6 +85,64 @@ const CHUNKING_STRATEGIES: ChunkingStrategy[] = [ 'parent_child', ]; +/** + * Group eyebrow: an uppercase, tracked title with a normal-weight muted ` · tag` + * suffix. Reads as more prominent than the individual field labels below it. + */ +function GroupHeader({ title, tag }: { title: string; tag: string }) { + return ( +

+ {title} + + {' · '} + {tag} + +

+ ); +} + +/** + * One settings row: a left block (medium-weight label plus optional muted + * description) and a right block holding the control. `alignStart` top-aligns + * the row for controls paired with a multi-line description; otherwise both + * sides are vertically centered. + */ +function SettingRow({ + label, + htmlFor, + description, + alignStart = false, + children, +}: { + label: string; + htmlFor?: string; + description?: ReactNode; + alignStart?: boolean; + children: ReactNode; +}) { + return ( +
+
+ + {description ? ( +

{description}

+ ) : null} +
+
{children}
+
+ ); +} + /** * Hydrate the form value from a stored (possibly partial/absent) SourceConfig, * filling every missing field with the documented default. Lenient on read so a @@ -238,277 +298,258 @@ export default function RetrievalOptions({ const body = (
{/* Retrieval group (live) */} -
-
-

- {tr('retrieval.title')} -

-

- {tr('retrieval.note')} -

-
+
+ - - setRetrieval({ chunks: Math.max(1, Number(e.target.value) || 1) }) - } - /> +
+ + + setRetrieval({ + chunks: Math.max(1, Number(e.target.value) || 1), + }) + } + /> + -
- { - const raw = e.target.value; - setRetrieval({ - score_threshold: raw === '' ? null : Number(raw), - }); - }} - /> -

- {tr('retrieval.scoreThresholdHint')} -

-
- -
- - - setRetrieval({ rephrase_query: checked }) - } - /> -
- -
- - -
- -
- - -

- {tr('retrieval.exposureHint')} -

-
+ { + const raw = e.target.value; + setRetrieval({ + score_threshold: raw === '' ? null : Number(raw), + }); + }} + /> + - {/* Prescreen sub-group */} -
-
- + + + setRetrieval({ rephrase_query: checked }) + } + /> + + + + + + + setPrescreen({ enabled: checked })} /> -
-

- {tr('prescreen.warning')} -

- {value.retrieval.prescreen.enabled && ( -
- - setPrescreen({ - candidate_k: Math.max( - value.retrieval.chunks, - Number(e.target.value) || value.retrieval.chunks, - ), - }) - } - /> - - setPrescreen({ - max_keep: Math.min( - value.retrieval.prescreen.candidate_k, - Math.max(1, Number(e.target.value) || 1), - ), - }) - } - /> - - setPrescreen({ - batch_size: Math.max(1, Number(e.target.value) || 1), - }) - } - /> - - setPrescreen({ model: e.target.value || null }) - } - /> -
- )} +
+ + {/* Prescreen expanded inputs (kept as floating-label cards) */} + {value.retrieval.prescreen.enabled && ( +
+ + setPrescreen({ + candidate_k: Math.max( + value.retrieval.chunks, + Number(e.target.value) || value.retrieval.chunks, + ), + }) + } + /> + + setPrescreen({ + max_keep: Math.min( + value.retrieval.prescreen.candidate_k, + Math.max(1, Number(e.target.value) || 1), + ), + }) + } + /> + + setPrescreen({ + batch_size: Math.max(1, Number(e.target.value) || 1), + }) + } + /> + setPrescreen({ model: e.target.value || null })} + /> +
+ )}
-
- {/* Chunking group (re-ingest required) */} -
-
-

- {tr('chunking.title')} -

-

- {tr('chunking.note')} -

-
+
+ -
- - -
+ + + + + {strategyOptions.map((opt) => ( + + {opt.label} + + ))} + + + - - setChunking({ - max_tokens: Math.max(1, Number(e.target.value) || 1), - }) - } - /> - - setChunking({ - min_tokens: Math.max(0, Number(e.target.value) || 0), - }) - } - /> -
- - - setChunking({ duplicate_headers: checked }) - } - /> + + + setChunking({ + max_tokens: Math.max(1, Number(e.target.value) || 1), + }) + } + /> + + + + + setChunking({ + min_tokens: Math.max(0, Number(e.target.value) || 0), + }) + } + /> + + + + + setChunking({ duplicate_headers: checked }) + } + /> +