mirror of
https://github.com/tiennm99/DocsGPT.git
synced 2026-10-10 08:16:59 +00:00
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:
1 parent
e18886b4f1
commit
f2e6967ec9
2 files changed
+296
-253
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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 (
|
||||
<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,
|
||||
* filling every missing field with the documented default. Lenient on read so a
|
||||
@@ -238,277 +298,258 @@ export default function RetrievalOptions({
|
||||
const body = (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Retrieval group (live) */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<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>
|
||||
<div className="flex flex-col gap-3">
|
||||
<GroupHeader title={tr('retrieval.title')} tag={tr('retrieval.tag')} />
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
label={tr('retrieval.chunks')}
|
||||
value={String(value.retrieval.chunks)}
|
||||
disabled={disabled}
|
||||
labelBgClassName="bg-card"
|
||||
onChange={(e) =>
|
||||
setRetrieval({ chunks: Math.max(1, Number(e.target.value) || 1) })
|
||||
}
|
||||
/>
|
||||
<div className="divide-border/50 divide-y">
|
||||
<SettingRow label={tr('retrieval.chunks')} htmlFor="retrieval-chunks">
|
||||
<Input
|
||||
id="retrieval-chunks"
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-24 text-right"
|
||||
value={String(value.retrieval.chunks)}
|
||||
disabled={disabled}
|
||||
onChange={(e) =>
|
||||
setRetrieval({
|
||||
chunks: Math.max(1, Number(e.target.value) || 1),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Input
|
||||
type="number"
|
||||
step="0.01"
|
||||
<SettingRow
|
||||
label={tr('retrieval.scoreThreshold')}
|
||||
value={
|
||||
value.retrieval.score_threshold === null
|
||||
? ''
|
||||
: 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 })
|
||||
}
|
||||
htmlFor="retrieval-score-threshold"
|
||||
description={tr('retrieval.scoreThresholdHint')}
|
||||
alignStart
|
||||
>
|
||||
<SelectTrigger
|
||||
id="retrieval-exposure"
|
||||
className="w-full 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>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{tr('retrieval.exposureHint')}
|
||||
</p>
|
||||
</div>
|
||||
<Input
|
||||
id="retrieval-score-threshold"
|
||||
type="number"
|
||||
step="0.01"
|
||||
className="w-24 text-right"
|
||||
value={
|
||||
value.retrieval.score_threshold === null
|
||||
? ''
|
||||
: String(value.retrieval.score_threshold)
|
||||
}
|
||||
disabled={disabled}
|
||||
placeholder={tr('retrieval.scoreThresholdPlaceholder')}
|
||||
onChange={(e) => {
|
||||
const raw = e.target.value;
|
||||
setRetrieval({
|
||||
score_threshold: raw === '' ? null : Number(raw),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
{/* Prescreen sub-group */}
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex flex-row items-center justify-between gap-3">
|
||||
<Label htmlFor="retrieval-prescreen" className="text-foreground">
|
||||
{tr('prescreen.enable')}
|
||||
</Label>
|
||||
<SettingRow
|
||||
label={tr('retrieval.rephraseQuery')}
|
||||
htmlFor="retrieval-rephrase"
|
||||
>
|
||||
<Switch
|
||||
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
|
||||
id="retrieval-prescreen"
|
||||
checked={value.retrieval.prescreen.enabled}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(checked) => setPrescreen({ enabled: checked })}
|
||||
/>
|
||||
</div>
|
||||
<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>
|
||||
)}
|
||||
</SettingRow>
|
||||
</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>
|
||||
|
||||
<hr className="border-border/60" />
|
||||
|
||||
{/* Chunking group (re-ingest required) */}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div>
|
||||
<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-3">
|
||||
<GroupHeader title={tr('chunking.title')} tag={tr('chunking.tag')} />
|
||||
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="chunking-strategy" className="text-foreground">
|
||||
{tr('chunking.strategy')}
|
||||
</Label>
|
||||
<Select
|
||||
value={value.chunking.strategy}
|
||||
disabled={disabled}
|
||||
onValueChange={(v) =>
|
||||
setChunking({ strategy: v as ChunkingStrategy })
|
||||
}
|
||||
<div className="divide-border/50 divide-y">
|
||||
<SettingRow
|
||||
label={tr('chunking.strategy')}
|
||||
htmlFor="chunking-strategy"
|
||||
>
|
||||
<SelectTrigger
|
||||
id="chunking-strategy"
|
||||
className="w-full rounded-md"
|
||||
size="lg"
|
||||
<Select
|
||||
value={value.chunking.strategy}
|
||||
disabled={disabled}
|
||||
onValueChange={(v) =>
|
||||
setChunking({ strategy: v as ChunkingStrategy })
|
||||
}
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{strategyOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<SelectTrigger
|
||||
id="chunking-strategy"
|
||||
className="w-52 rounded-md"
|
||||
size="lg"
|
||||
>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{strategyOptions.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</SettingRow>
|
||||
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
label={tr('chunking.maxTokens')}
|
||||
value={String(value.chunking.max_tokens)}
|
||||
disabled={disabled}
|
||||
labelBgClassName="bg-card"
|
||||
onChange={(e) =>
|
||||
setChunking({
|
||||
max_tokens: Math.max(1, Number(e.target.value) || 1),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
min={0}
|
||||
label={tr('chunking.minTokens')}
|
||||
value={String(value.chunking.min_tokens)}
|
||||
disabled={disabled}
|
||||
labelBgClassName="bg-card"
|
||||
onChange={(e) =>
|
||||
setChunking({
|
||||
min_tokens: Math.max(0, Number(e.target.value) || 0),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<div className="flex flex-row items-center justify-between gap-3">
|
||||
<Label htmlFor="chunking-dup-headers" className="text-foreground">
|
||||
{tr('chunking.duplicateHeaders')}
|
||||
</Label>
|
||||
<Switch
|
||||
id="chunking-dup-headers"
|
||||
checked={value.chunking.duplicate_headers}
|
||||
disabled={disabled}
|
||||
onCheckedChange={(checked) =>
|
||||
setChunking({ duplicate_headers: checked })
|
||||
}
|
||||
/>
|
||||
<SettingRow
|
||||
label={tr('chunking.maxTokens')}
|
||||
htmlFor="chunking-max-tokens"
|
||||
>
|
||||
<Input
|
||||
id="chunking-max-tokens"
|
||||
type="number"
|
||||
min={1}
|
||||
className="w-24 text-right"
|
||||
value={String(value.chunking.max_tokens)}
|
||||
disabled={disabled}
|
||||
onChange={(e) =>
|
||||
setChunking({
|
||||
max_tokens: Math.max(1, Number(e.target.value) || 1),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow
|
||||
label={tr('chunking.minTokens')}
|
||||
htmlFor="chunking-min-tokens"
|
||||
>
|
||||
<Input
|
||||
id="chunking-min-tokens"
|
||||
type="number"
|
||||
min={0}
|
||||
className="w-24 text-right"
|
||||
value={String(value.chunking.min_tokens)}
|
||||
disabled={disabled}
|
||||
onChange={(e) =>
|
||||
setChunking({
|
||||
min_tokens: Math.max(0, Number(e.target.value) || 0),
|
||||
})
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
|
||||
Reference in new issue
Block a user