feat(webui): segmented default/temperature/top_p sampling picker in the chat bar

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
m4
2026-07-30 10:41:32 +08:00
parent a4202e149a
commit cfaf704652
+136 -49
View File
@@ -101,7 +101,11 @@ import { WorkspaceFileDialog } from "@/app/components/WorkspaceFileDialog";
import { MemoryFileDialog } from "@/app/components/MemoryFileDialog";
import { FILE_LINK_EVENT, type FileLinkEventDetail } from "@/lib/fileLink";
import { notifyWorkspaceChanged } from "@/lib/workspaceEvents";
import type { ModelRef, ReasoningEffort } from "@/lib/modelRegistry";
import type {
ModelRef,
ReasoningEffort,
SamplingOverride,
} from "@/lib/modelRegistry";
import {
modelRefLabel,
useAvailableModels,
@@ -2255,7 +2259,7 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
if (modelSelection === "inherit") return;
try {
await setModelSelection({
primary: modelSelection.primary,
...modelSelection,
reasoning_effort: effort,
});
toast.success(
@@ -2274,70 +2278,36 @@ export const ChatInterface = React.memo<ChatInterfaceProps>(
/>
)}
{currentModel && (
<GenerationParamSlider
ariaLabel="Temperature"
<SamplingOverridePicker
disabled={modelSelection === "inherit"}
override={
modelSelection === "inherit"
? null
: (modelSelection.temperature ?? null)
: (modelSelection.sampling_override ?? null)
}
fallback={selectedCatalogEntry?.default_temperature ?? null}
min={0}
max={2}
step={0.05}
fallbackTemperature={
selectedCatalogEntry?.default_temperature ?? null
}
fallbackTopP={selectedCatalogEntry?.default_top_p ?? null}
onCommit={async (value) => {
if (modelSelection === "inherit") return;
try {
await setModelSelection({
...modelSelection,
temperature: value,
sampling_override: value,
});
toast.success(
value === null
? "Temperature back to the registry default."
: `Temperature set to ${value.toFixed(2)}.`
? "Sampling back to the registry default."
: value.kind === "temperature"
? `Temperature set to ${value.value.toFixed(2)} (top_p omitted).`
: `Top P set to ${value.value.toFixed(2)} (temperature omitted).`
);
} catch (err) {
toast.error(
err instanceof Error
? `Couldn't update temperature: ${err.message}`
: "Couldn't update temperature — try again."
);
}
}}
/>
)}
{currentModel && (
<GenerationParamSlider
ariaLabel="Top P"
disabled={modelSelection === "inherit"}
override={
modelSelection === "inherit"
? null
: (modelSelection.top_p ?? null)
}
fallback={selectedCatalogEntry?.default_top_p ?? null}
min={0}
max={1}
step={0.05}
onCommit={async (value) => {
if (modelSelection === "inherit") return;
try {
await setModelSelection({
...modelSelection,
top_p: value,
});
toast.success(
value === null
? "Top P back to the registry default."
: `Top P set to ${value.toFixed(2)}.`
);
} catch (err) {
toast.error(
err instanceof Error
? `Couldn't update top P: ${err.message}`
: "Couldn't update top P — try again."
? `Couldn't update sampling: ${err.message}`
: "Couldn't update sampling — try again."
);
}
}}
@@ -2777,3 +2747,120 @@ function GenerationParamSlider({
</span>
);
}
function SamplingOverridePicker({
disabled,
override,
fallbackTemperature,
fallbackTopP,
onCommit,
}: {
disabled: boolean;
/** Current per-thread override; null means the registry defaults apply. */
override: SamplingOverride | null;
/** Registry-effective defaults used to anchor each slider. */
fallbackTemperature: number | null;
fallbackTopP: number | null;
onCommit: (value: SamplingOverride | null) => void | Promise<void>;
}) {
const mode: "default" | "temperature" | "top_p" = override?.kind ?? "default";
const select = (next: "default" | "temperature" | "top_p") => {
if (next === mode) return;
if (next === "default") {
void onCommit(null);
return;
}
const value =
next === "temperature"
? (override?.kind === "temperature" ? override.value : null) ??
fallbackTemperature ??
1
: (override?.kind === "top_p" ? override.value : null) ??
fallbackTopP ??
1;
void onCommit({ kind: next, value });
};
const modes = [
{ key: "default", label: "默认" },
{ key: "temperature", label: "T" },
{ key: "top_p", label: "P" },
] as const;
return (
<span
className={cn("flex items-center gap-1.5", disabled && "opacity-50")}
title={
disabled
? "Pick a model to override the registry-configured sampling"
: "Sampling override: temperature and top_p are mutually exclusive"
}
>
{/* ≥sm: segmented control */}
<span className="hidden sm:flex items-center overflow-hidden rounded border border-border">
{modes.map(({ key, label }) => (
<button
key={key}
type="button"
disabled={disabled}
aria-pressed={mode === key}
onClick={() => select(key)}
className={cn(
"px-1.5 py-0.5 text-xs transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed",
mode === key
? "bg-accent font-medium text-foreground"
: "text-muted-foreground hover:bg-accent/60"
)}
>
{label}
</button>
))}
</span>
{/* <sm: dropdown */}
<select
aria-label="Sampling override"
disabled={disabled}
value={mode}
onChange={(event) =>
select(event.target.value as "default" | "temperature" | "top_p")
}
className="sm:hidden rounded border border-border bg-transparent px-1 py-0.5 text-xs text-muted-foreground"
>
{modes.map(({ key, label }) => (
<option key={key} value={key}>
{label}
</option>
))}
</select>
{mode === "temperature" && (
<GenerationParamSlider
ariaLabel="Temperature"
disabled={disabled}
override={override?.kind === "temperature" ? override.value : null}
fallback={fallbackTemperature}
min={0}
max={2}
step={0.05}
onCommit={(value) =>
void onCommit(value === null ? null : { kind: "temperature", value })
}
/>
)}
{mode === "top_p" && (
<GenerationParamSlider
ariaLabel="Top P"
disabled={disabled}
override={override?.kind === "top_p" ? override.value : null}
fallback={fallbackTopP}
min={0}
max={1}
step={0.05}
onCommit={(value) =>
void onCommit(value === null ? null : { kind: "top_p", value })
}
/>
)}
</span>
);
}