feat: redesign advanced settings panel as settings rows with dividers

Group eyebrow headers with short live/re-ingest tags, lighter field labels, hairline dividers between rows, compact right-aligned controls, and hide the single-option Retriever select.
This commit is contained in:
Alex committed 2026-06-21 14:15:09 +01:00
1 parent e18886b4f1
commit f2e6967ec9
2 files changed
+296 -253

No files matched your search

+2
View File
@@ -137,6 +137,7 @@
"retrieval": { "retrieval": {
"title": "Retrieval", "title": "Retrieval",
"note": "These changes take effect immediately, no re-ingest needed.", "note": "These changes take effect immediately, no re-ingest needed.",
"tag": "live · no re-ingest",
"chunks": "Chunks to retrieve (top-k)", "chunks": "Chunks to retrieve (top-k)",
"scoreThreshold": "Score threshold", "scoreThreshold": "Score threshold",
"scoreThresholdPlaceholder": "None", "scoreThresholdPlaceholder": "None",
@@ -165,6 +166,7 @@
"chunking": { "chunking": {
"title": "Chunking", "title": "Chunking",
"note": "Ingest-time settings. Changing these requires re-ingesting the source.", "note": "Ingest-time settings. Changing these requires re-ingesting the source.",
"tag": "re-ingest to apply",
"strategy": "Chunking strategy", "strategy": "Chunking strategy",
"strategies": { "strategies": {
"classic_chunk": "Classic", "classic_chunk": "Classic",
@@ -1,6 +1,8 @@
import { useMemo, useState } from 'react'; import { type ReactNode, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { cn } from '@/lib/utils';
import { Button } from '../../components/ui/button'; import { Button } from '../../components/ui/button';
import { Input } from '../../components/ui/input'; import { Input } from '../../components/ui/input';
import { Label } from '../../components/ui/label'; import { Label } from '../../components/ui/label';
@@ -83,6 +85,64 @@ const CHUNKING_STRATEGIES: ChunkingStrategy[] = [
'parent_child', '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 (
<h4 className="text-foreground text-xs font-semibold tracking-wider uppercase">
{title}
<span className="text-muted-foreground font-normal normal-case">
{' · '}
{tag}
</span>
</h4>
);
}
/**
* 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 (
<div
className={cn(
'flex flex-row justify-between gap-4 py-3 first:pt-0 last:pb-0',
alignStart ? 'items-start' : 'items-center',
)}
>
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<Label
htmlFor={htmlFor}
className="text-foreground text-sm font-medium"
>
{label}
</Label>
{description ? (
<p className="text-muted-foreground text-xs">{description}</p>
) : null}
</div>
<div className="shrink-0">{children}</div>
</div>
);
}
/** /**
* Hydrate the form value from a stored (possibly partial/absent) SourceConfig, * Hydrate the form value from a stored (possibly partial/absent) SourceConfig,
* filling every missing field with the documented default. Lenient on read so a * filling every missing field with the documented default. Lenient on read so a
@@ -238,277 +298,258 @@ export default function RetrievalOptions({
const body = ( const body = (
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
{/* Retrieval group (live) */} {/* Retrieval group (live) */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-3">
<div> <GroupHeader title={tr('retrieval.title')} tag={tr('retrieval.tag')} />
<h4 className="text-foreground text-sm font-semibold">
{tr('retrieval.title')}
</h4>
<p className="text-muted-foreground mt-0.5 text-xs">
{tr('retrieval.note')}
</p>
</div>
<Input <div className="divide-border/50 divide-y">
type="number" <SettingRow label={tr('retrieval.chunks')} htmlFor="retrieval-chunks">
min={1} <Input
label={tr('retrieval.chunks')} id="retrieval-chunks"
value={String(value.retrieval.chunks)} type="number"
disabled={disabled} min={1}
labelBgClassName="bg-card" className="w-24 text-right"
onChange={(e) => value={String(value.retrieval.chunks)}
setRetrieval({ chunks: Math.max(1, Number(e.target.value) || 1) }) disabled={disabled}
} onChange={(e) =>
/> setRetrieval({
chunks: Math.max(1, Number(e.target.value) || 1),
})
}
/>
</SettingRow>
<div className="flex flex-col gap-1.5"> <SettingRow
<Input
type="number"
step="0.01"
label={tr('retrieval.scoreThreshold')} label={tr('retrieval.scoreThreshold')}
value={ htmlFor="retrieval-score-threshold"
value.retrieval.score_threshold === null description={tr('retrieval.scoreThresholdHint')}
? '' alignStart
: String(value.retrieval.score_threshold)
}
disabled={disabled}
labelBgClassName="bg-card"
placeholder={tr('retrieval.scoreThresholdPlaceholder')}
onChange={(e) => {
const raw = e.target.value;
setRetrieval({
score_threshold: raw === '' ? null : Number(raw),
});
}}
/>
<p className="text-muted-foreground text-xs">
{tr('retrieval.scoreThresholdHint')}
</p>
</div>
<div className="flex flex-row items-center justify-between gap-3">
<Label htmlFor="retrieval-rephrase" className="text-foreground">
{tr('retrieval.rephraseQuery')}
</Label>
<Switch
id="retrieval-rephrase"
checked={value.retrieval.rephrase_query}
disabled={disabled}
onCheckedChange={(checked) =>
setRetrieval({ rephrase_query: checked })
}
/>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="retrieval-retriever" className="text-foreground">
{tr('retrieval.retriever')}
</Label>
<Select value="classic" disabled>
<SelectTrigger
id="retrieval-retriever"
className="w-full rounded-md"
size="lg"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="classic">
{tr('retrieval.retrievers.classic')}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex flex-col gap-1.5">
<Label htmlFor="retrieval-exposure" className="text-foreground">
{tr('retrieval.exposure')}
</Label>
<Select
value={value.retrieval.exposure}
disabled={disabled}
onValueChange={(v) =>
setRetrieval({ exposure: v as RetrievalExposure })
}
> >
<SelectTrigger <Input
id="retrieval-exposure" id="retrieval-score-threshold"
className="w-full rounded-md" type="number"
size="lg" step="0.01"
> className="w-24 text-right"
<SelectValue /> value={
</SelectTrigger> value.retrieval.score_threshold === null
<SelectContent> ? ''
<SelectItem value="prefetch"> : String(value.retrieval.score_threshold)
{tr('retrieval.exposures.prefetch')} }
</SelectItem> disabled={disabled}
<SelectItem value="agentic_tool"> placeholder={tr('retrieval.scoreThresholdPlaceholder')}
{tr('retrieval.exposures.agentic_tool')} onChange={(e) => {
</SelectItem> const raw = e.target.value;
</SelectContent> setRetrieval({
</Select> score_threshold: raw === '' ? null : Number(raw),
<p className="text-muted-foreground text-xs"> });
{tr('retrieval.exposureHint')} }}
</p> />
</div> </SettingRow>
{/* Prescreen sub-group */} <SettingRow
<div className="flex flex-col gap-3"> label={tr('retrieval.rephraseQuery')}
<div className="flex flex-row items-center justify-between gap-3"> htmlFor="retrieval-rephrase"
<Label htmlFor="retrieval-prescreen" className="text-foreground"> >
{tr('prescreen.enable')} <Switch
</Label> id="retrieval-rephrase"
checked={value.retrieval.rephrase_query}
disabled={disabled}
onCheckedChange={(checked) =>
setRetrieval({ rephrase_query: checked })
}
/>
</SettingRow>
<SettingRow
label={tr('retrieval.exposure')}
htmlFor="retrieval-exposure"
description={tr('retrieval.exposureHint')}
alignStart
>
<Select
value={value.retrieval.exposure}
disabled={disabled}
onValueChange={(v) =>
setRetrieval({ exposure: v as RetrievalExposure })
}
>
<SelectTrigger
id="retrieval-exposure"
className="w-52 rounded-md"
size="lg"
>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="prefetch">
{tr('retrieval.exposures.prefetch')}
</SelectItem>
<SelectItem value="agentic_tool">
{tr('retrieval.exposures.agentic_tool')}
</SelectItem>
</SelectContent>
</Select>
</SettingRow>
<SettingRow
label={tr('prescreen.enable')}
htmlFor="retrieval-prescreen"
description={tr('prescreen.warning')}
alignStart
>
<Switch <Switch
id="retrieval-prescreen" id="retrieval-prescreen"
checked={value.retrieval.prescreen.enabled} checked={value.retrieval.prescreen.enabled}
disabled={disabled} disabled={disabled}
onCheckedChange={(checked) => setPrescreen({ enabled: checked })} onCheckedChange={(checked) => setPrescreen({ enabled: checked })}
/> />
</div> </SettingRow>
<p className="text-muted-foreground text-xs">
{tr('prescreen.warning')}
</p>
{value.retrieval.prescreen.enabled && (
<div className="border-border flex flex-col gap-4 rounded-lg border p-4">
<Input
type="number"
min={value.retrieval.chunks}
label={tr('prescreen.candidateK')}
value={String(value.retrieval.prescreen.candidate_k)}
disabled={disabled}
labelBgClassName="bg-card"
onChange={(e) =>
setPrescreen({
candidate_k: Math.max(
value.retrieval.chunks,
Number(e.target.value) || value.retrieval.chunks,
),
})
}
/>
<Input
type="number"
min={1}
label={tr('prescreen.maxKeep')}
value={String(value.retrieval.prescreen.max_keep)}
disabled={disabled}
labelBgClassName="bg-card"
onChange={(e) =>
setPrescreen({
max_keep: Math.min(
value.retrieval.prescreen.candidate_k,
Math.max(1, Number(e.target.value) || 1),
),
})
}
/>
<Input
type="number"
min={1}
label={tr('prescreen.batchSize')}
value={String(value.retrieval.prescreen.batch_size)}
disabled={disabled}
labelBgClassName="bg-card"
onChange={(e) =>
setPrescreen({
batch_size: Math.max(1, Number(e.target.value) || 1),
})
}
/>
<Input
type="text"
label={tr('prescreen.model')}
value={value.retrieval.prescreen.model ?? ''}
disabled={disabled}
labelBgClassName="bg-card"
placeholder={tr('prescreen.modelPlaceholder')}
onChange={(e) =>
setPrescreen({ model: e.target.value || null })
}
/>
</div>
)}
</div> </div>
{/* Prescreen expanded inputs (kept as floating-label cards) */}
{value.retrieval.prescreen.enabled && (
<div className="border-border ml-4 flex flex-col gap-4 rounded-lg border p-4">
<Input
type="number"
min={value.retrieval.chunks}
label={tr('prescreen.candidateK')}
value={String(value.retrieval.prescreen.candidate_k)}
disabled={disabled}
labelBgClassName="bg-card"
onChange={(e) =>
setPrescreen({
candidate_k: Math.max(
value.retrieval.chunks,
Number(e.target.value) || value.retrieval.chunks,
),
})
}
/>
<Input
type="number"
min={1}
label={tr('prescreen.maxKeep')}
value={String(value.retrieval.prescreen.max_keep)}
disabled={disabled}
labelBgClassName="bg-card"
onChange={(e) =>
setPrescreen({
max_keep: Math.min(
value.retrieval.prescreen.candidate_k,
Math.max(1, Number(e.target.value) || 1),
),
})
}
/>
<Input
type="number"
min={1}
label={tr('prescreen.batchSize')}
value={String(value.retrieval.prescreen.batch_size)}
disabled={disabled}
labelBgClassName="bg-card"
onChange={(e) =>
setPrescreen({
batch_size: Math.max(1, Number(e.target.value) || 1),
})
}
/>
<Input
type="text"
label={tr('prescreen.model')}
value={value.retrieval.prescreen.model ?? ''}
disabled={disabled}
labelBgClassName="bg-card"
placeholder={tr('prescreen.modelPlaceholder')}
onChange={(e) => setPrescreen({ model: e.target.value || null })}
/>
</div>
)}
</div> </div>
<hr className="border-border/60" />
{/* Chunking group (re-ingest required) */} {/* Chunking group (re-ingest required) */}
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-3">
<div> <GroupHeader title={tr('chunking.title')} tag={tr('chunking.tag')} />
<h4 className="text-foreground text-sm font-semibold">
{tr('chunking.title')}
</h4>
<p className="text-muted-foreground mt-0.5 text-xs">
{tr('chunking.note')}
</p>
</div>
<div className="flex flex-col gap-1.5"> <div className="divide-border/50 divide-y">
<Label htmlFor="chunking-strategy" className="text-foreground"> <SettingRow
{tr('chunking.strategy')} label={tr('chunking.strategy')}
</Label> htmlFor="chunking-strategy"
<Select
value={value.chunking.strategy}
disabled={disabled}
onValueChange={(v) =>
setChunking({ strategy: v as ChunkingStrategy })
}
> >
<SelectTrigger <Select
id="chunking-strategy" value={value.chunking.strategy}
className="w-full rounded-md" disabled={disabled}
size="lg" onValueChange={(v) =>
setChunking({ strategy: v as ChunkingStrategy })
}
> >
<SelectValue /> <SelectTrigger
</SelectTrigger> id="chunking-strategy"
<SelectContent> className="w-52 rounded-md"
{strategyOptions.map((opt) => ( size="lg"
<SelectItem key={opt.value} value={opt.value}> >
{opt.label} <SelectValue />
</SelectItem> </SelectTrigger>
))} <SelectContent>
</SelectContent> {strategyOptions.map((opt) => (
</Select> <SelectItem key={opt.value} value={opt.value}>
</div> {opt.label}
</SelectItem>
))}
</SelectContent>
</Select>
</SettingRow>
<Input <SettingRow
type="number" label={tr('chunking.maxTokens')}
min={1} htmlFor="chunking-max-tokens"
label={tr('chunking.maxTokens')} >
value={String(value.chunking.max_tokens)} <Input
disabled={disabled} id="chunking-max-tokens"
labelBgClassName="bg-card" type="number"
onChange={(e) => min={1}
setChunking({ className="w-24 text-right"
max_tokens: Math.max(1, Number(e.target.value) || 1), value={String(value.chunking.max_tokens)}
}) disabled={disabled}
} onChange={(e) =>
/> setChunking({
<Input max_tokens: Math.max(1, Number(e.target.value) || 1),
type="number" })
min={0} }
label={tr('chunking.minTokens')} />
value={String(value.chunking.min_tokens)} </SettingRow>
disabled={disabled}
labelBgClassName="bg-card" <SettingRow
onChange={(e) => label={tr('chunking.minTokens')}
setChunking({ htmlFor="chunking-min-tokens"
min_tokens: Math.max(0, Number(e.target.value) || 0), >
}) <Input
} id="chunking-min-tokens"
/> type="number"
<div className="flex flex-row items-center justify-between gap-3"> min={0}
<Label htmlFor="chunking-dup-headers" className="text-foreground"> className="w-24 text-right"
{tr('chunking.duplicateHeaders')} value={String(value.chunking.min_tokens)}
</Label> disabled={disabled}
<Switch onChange={(e) =>
id="chunking-dup-headers" setChunking({
checked={value.chunking.duplicate_headers} min_tokens: Math.max(0, Number(e.target.value) || 0),
disabled={disabled} })
onCheckedChange={(checked) => }
setChunking({ duplicate_headers: checked }) />
} </SettingRow>
/>
<SettingRow
label={tr('chunking.duplicateHeaders')}
htmlFor="chunking-dup-headers"
>
<Switch
id="chunking-dup-headers"
checked={value.chunking.duplicate_headers}
disabled={disabled}
onCheckedChange={(checked) =>
setChunking({ duplicate_headers: checked })
}
/>
</SettingRow>
</div> </div>
</div> </div>
</div> </div>