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:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user