fix(models): show inline preset name conflicts
This commit is contained in:
@@ -137,6 +137,7 @@ export function SettingsPage({
|
||||
modelPresetBeforeCreateRef,
|
||||
modelPresetCreating,
|
||||
modelPresetEditingName,
|
||||
modelPresetNameError,
|
||||
modelPresetPendingDelete,
|
||||
nanobotFeatureAction,
|
||||
nanobotFeatureConfirm,
|
||||
@@ -187,6 +188,7 @@ export function SettingsPage({
|
||||
setMcpOAuthCallbackUrl,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetNameError,
|
||||
setModelPresetPendingDelete,
|
||||
setNanobotFeatureConfirm,
|
||||
setNanobotFeatures,
|
||||
@@ -243,6 +245,7 @@ export function SettingsPage({
|
||||
form={form}
|
||||
setForm={setForm}
|
||||
editingPresetName={modelPresetEditingName}
|
||||
presetNameError={modelPresetNameError}
|
||||
settings={settings}
|
||||
dirty={modelDirty}
|
||||
creating={modelPresetCreating}
|
||||
@@ -259,9 +262,11 @@ export function SettingsPage({
|
||||
onMigrate={handleMigrateModelConfigurations}
|
||||
onBeginCreate={beginModelPresetCreation}
|
||||
onCancelCreate={cancelModelPresetCreation}
|
||||
onClearPresetNameError={() => setModelPresetNameError(null)}
|
||||
onSelectConfiguration={(name) => {
|
||||
setModelPresetCreating(false);
|
||||
setModelPresetEditingName(name);
|
||||
setModelPresetNameError(null);
|
||||
modelPresetBeforeCreateRef.current = null;
|
||||
}}
|
||||
onDeleteConfiguration={setModelPresetPendingDelete}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import { useEffect, useRef, useState, type Dispatch, type SetStateAction } from "react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
@@ -160,6 +160,7 @@ export function ModelsSettings({
|
||||
form,
|
||||
setForm,
|
||||
editingPresetName,
|
||||
presetNameError,
|
||||
settings,
|
||||
dirty,
|
||||
creating,
|
||||
@@ -176,6 +177,7 @@ export function ModelsSettings({
|
||||
onMigrate,
|
||||
onBeginCreate,
|
||||
onCancelCreate,
|
||||
onClearPresetNameError,
|
||||
onSelectConfiguration,
|
||||
onDeleteConfiguration,
|
||||
}: {
|
||||
@@ -183,6 +185,7 @@ export function ModelsSettings({
|
||||
form: AgentSettingsDraft;
|
||||
setForm: Dispatch<SetStateAction<AgentSettingsDraft>>;
|
||||
editingPresetName: string;
|
||||
presetNameError: string | null;
|
||||
settings: SettingsPayload;
|
||||
dirty: boolean;
|
||||
creating: boolean;
|
||||
@@ -199,6 +202,7 @@ export function ModelsSettings({
|
||||
onMigrate: () => void;
|
||||
onBeginCreate: () => void;
|
||||
onCancelCreate: () => void;
|
||||
onClearPresetNameError: () => void;
|
||||
onSelectConfiguration: (name: string) => void;
|
||||
onDeleteConfiguration: (preset: SettingsPayload["model_presets"][number]) => void;
|
||||
}) {
|
||||
@@ -206,10 +210,15 @@ export function ModelsSettings({
|
||||
const tx = (key: string, fallback: string, values?: Record<string, unknown>) =>
|
||||
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
||||
const [editorOpen, setEditorOpen] = useState(false);
|
||||
const presetNameInputRef = useRef<HTMLInputElement>(null);
|
||||
const [editorRowKey, setEditorRowKey] = useState<string | null>(null);
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [draggedCallOrderIndex, setDraggedCallOrderIndex] = useState<number | null>(null);
|
||||
const [dragOverCallOrderIndex, setDragOverCallOrderIndex] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (presetNameError) presetNameInputRef.current?.focus();
|
||||
}, [presetNameError]);
|
||||
const namedPresets = settings.model_presets.filter((preset) => !preset.is_default);
|
||||
const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset]));
|
||||
const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name));
|
||||
@@ -355,16 +364,40 @@ export function ModelsSettings({
|
||||
"Used everywhere, including /model commands. Names must be unique.",
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
autoFocus={creating}
|
||||
aria-label={tx("settings.models.presetName", "Preset name")}
|
||||
value={form.modelPreset}
|
||||
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
|
||||
onChange={(event) =>
|
||||
setForm((prev) => ({ ...prev, modelPreset: event.target.value }))
|
||||
}
|
||||
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"w-[min(280px,70vw)] motion-reduce:animate-none",
|
||||
presetNameError && "animate-[preset-name-shake_180ms_ease-in-out]",
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
ref={presetNameInputRef}
|
||||
autoFocus={creating}
|
||||
aria-label={tx("settings.models.presetName", "Preset name")}
|
||||
aria-invalid={Boolean(presetNameError)}
|
||||
aria-describedby={presetNameError ? "model-preset-name-error" : undefined}
|
||||
value={form.modelPreset}
|
||||
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
|
||||
onChange={(event) => {
|
||||
onClearPresetNameError();
|
||||
setForm((prev) => ({ ...prev, modelPreset: event.target.value }));
|
||||
}}
|
||||
className={cn(
|
||||
"h-8 rounded-full text-[13px]",
|
||||
presetNameError &&
|
||||
"border-destructive/70 focus-visible:border-destructive focus-visible:ring-destructive/25",
|
||||
)}
|
||||
/>
|
||||
{presetNameError ? (
|
||||
<p
|
||||
id="model-preset-name-error"
|
||||
role="alert"
|
||||
className="mt-1.5 px-1 text-[12px] leading-4 text-destructive"
|
||||
>
|
||||
{presetNameError}
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</SettingsRow>
|
||||
<SettingsRow title={t("settings.rows.provider")}>
|
||||
<ProviderPicker
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import type { ModelSettingsState } from "@/components/settings/models/useModelSettingsState";
|
||||
import { normalizeContextWindowTokens } from "@/components/settings/shared/ModelControls";
|
||||
import {
|
||||
ApiError,
|
||||
completeProviderOAuth,
|
||||
createModelConfiguration,
|
||||
createProviderSettings,
|
||||
@@ -107,6 +108,7 @@ export function useModelSettingsActions({
|
||||
setModelMigrationSaving,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetNameError,
|
||||
setModelPresetPendingDelete,
|
||||
setProviderForms,
|
||||
setProviderOAuthCompleting,
|
||||
@@ -119,6 +121,33 @@ export function useModelSettingsActions({
|
||||
visibleProviderKeys,
|
||||
} = state;
|
||||
|
||||
const presetNameConflict = (name: string, currentName?: string) => {
|
||||
const normalized = name.toLowerCase();
|
||||
return settings?.model_presets.some(
|
||||
(preset) =>
|
||||
!preset.is_default &&
|
||||
preset.name !== currentName &&
|
||||
preset.name.toLowerCase() === normalized,
|
||||
) ?? false;
|
||||
};
|
||||
|
||||
const showPresetNameConflict = () => {
|
||||
setModelPresetNameError(
|
||||
t("settings.models.presetNameDuplicate", {
|
||||
defaultValue: "A preset with this name already exists.",
|
||||
}),
|
||||
);
|
||||
setError(null);
|
||||
};
|
||||
|
||||
const handlePresetSaveError = (reason: unknown) => {
|
||||
if (reason instanceof ApiError && reason.status === 409) {
|
||||
showPresetNameConflict();
|
||||
return;
|
||||
}
|
||||
setError((reason as Error).message);
|
||||
};
|
||||
|
||||
const saveModelSettings = async () => {
|
||||
if (
|
||||
!settings ||
|
||||
@@ -144,6 +173,11 @@ export function useModelSettingsActions({
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (presetNameConflict(name)) {
|
||||
showPresetNameConflict();
|
||||
return;
|
||||
}
|
||||
setModelPresetNameError(null);
|
||||
setModelConfigurationSaving(true);
|
||||
try {
|
||||
const payload = await createModelConfiguration(client, {
|
||||
@@ -175,9 +209,10 @@ export function useModelSettingsActions({
|
||||
}
|
||||
modelPresetBeforeCreateRef.current = null;
|
||||
onModelNameChange(finalPayload.agent.model || null);
|
||||
setModelPresetNameError(null);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
handlePresetSaveError(err);
|
||||
} finally {
|
||||
setModelConfigurationSaving(false);
|
||||
}
|
||||
@@ -190,6 +225,11 @@ export function useModelSettingsActions({
|
||||
);
|
||||
if (!selectedPreset) return;
|
||||
const nextName = form.modelPreset.trim();
|
||||
if (presetNameConflict(nextName, selectedPreset.name)) {
|
||||
showPresetNameConflict();
|
||||
return;
|
||||
}
|
||||
setModelPresetNameError(null);
|
||||
const reasoningEffort = form.reasoningEffort || null;
|
||||
setSaving(true);
|
||||
try {
|
||||
@@ -214,9 +254,10 @@ export function useModelSettingsActions({
|
||||
setForm(agentDraftFromPayload(payload, nextName));
|
||||
setModelPresetEditingName(nextName);
|
||||
onModelNameChange(payload.agent.model || null);
|
||||
setModelPresetNameError(null);
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
handlePresetSaveError(err);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -235,6 +276,7 @@ export function useModelSettingsActions({
|
||||
configuredModelProviderOptions[0]?.name ??
|
||||
"";
|
||||
modelPresetBeforeCreateRef.current = modelPresetEditingName;
|
||||
setModelPresetNameError(null);
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
modelPreset: "",
|
||||
@@ -254,6 +296,7 @@ export function useModelSettingsActions({
|
||||
if (!settings || modelConfigurationSaving) return;
|
||||
const previousPreset = modelPresetBeforeCreateRef.current;
|
||||
setModelPresetCreating(false);
|
||||
setModelPresetNameError(null);
|
||||
setForm(agentDraftFromPayload(settings, previousPreset ?? undefined));
|
||||
setModelPresetEditingName(previousPreset ?? agentDraftFromPayload(settings).modelPreset);
|
||||
modelPresetBeforeCreateRef.current = null;
|
||||
|
||||
@@ -14,6 +14,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
||||
: DEFAULT_AGENT_SETTINGS_DRAFT;
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [modelPresetCreating, setModelPresetCreating] = useState(false);
|
||||
const [modelPresetNameError, setModelPresetNameError] = useState<string | null>(null);
|
||||
const [modelConfigurationSaving, setModelConfigurationSaving] = useState(false);
|
||||
const [modelCallOrderSaving, setModelCallOrderSaving] = useState(false);
|
||||
const [modelMigrationSaving, setModelMigrationSaving] = useState(false);
|
||||
@@ -50,6 +51,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
||||
modelPresetBeforeCreateRef,
|
||||
modelPresetCreating,
|
||||
modelPresetEditingName,
|
||||
modelPresetNameError,
|
||||
modelPresetPendingDelete,
|
||||
providerForms,
|
||||
providerOAuthCompleting,
|
||||
@@ -68,6 +70,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
||||
setModelMigrationSaving,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetNameError,
|
||||
setModelPresetPendingDelete,
|
||||
setProviderForms,
|
||||
setProviderOAuthCompleting,
|
||||
|
||||
@@ -93,11 +93,11 @@ export function useSettingsController({
|
||||
const {
|
||||
editingProviderKeys, expandedProvider, form, modelCallOrder, modelCallOrderSaving,
|
||||
modelConfigurationSaving, modelMigrationSaving, modelPresetBeforeCreateRef,
|
||||
modelPresetCreating, modelPresetEditingName, modelPresetPendingDelete,
|
||||
modelPresetCreating, modelPresetEditingName, modelPresetNameError, modelPresetPendingDelete,
|
||||
providerForms, providerOAuthCompleting,
|
||||
providerOAuthDialogError, providerOAuthFlow, providerOAuthFlowRef, providerOAuthResponse,
|
||||
providerSaving, saving, setForm,
|
||||
setModelCallOrder, setModelPresetCreating, setModelPresetEditingName,
|
||||
setModelCallOrder, setModelPresetCreating, setModelPresetEditingName, setModelPresetNameError,
|
||||
setModelPresetPendingDelete,
|
||||
setProviderForms, setProviderOAuthCompleting, setProviderOAuthDialogError,
|
||||
setProviderOAuthFlow, setProviderOAuthResponse, visibleProviderKeys,
|
||||
@@ -538,6 +538,7 @@ export function useSettingsController({
|
||||
modelPresetBeforeCreateRef,
|
||||
modelPresetCreating,
|
||||
modelPresetEditingName,
|
||||
modelPresetNameError,
|
||||
modelPresetPendingDelete,
|
||||
nanobotFeatureAction,
|
||||
nanobotFeatureConfirm,
|
||||
@@ -588,6 +589,7 @@ export function useSettingsController({
|
||||
setMcpOAuthCallbackUrl,
|
||||
setModelPresetCreating,
|
||||
setModelPresetEditingName,
|
||||
setModelPresetNameError,
|
||||
setModelPresetPendingDelete,
|
||||
setNanobotFeatureConfirm,
|
||||
setNanobotFeatures,
|
||||
|
||||
Reference in New Issue
Block a user