mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
fix(models): show inline preset name conflicts
This commit is contained in:
@@ -137,6 +137,7 @@ export function SettingsPage({
|
|||||||
modelPresetBeforeCreateRef,
|
modelPresetBeforeCreateRef,
|
||||||
modelPresetCreating,
|
modelPresetCreating,
|
||||||
modelPresetEditingName,
|
modelPresetEditingName,
|
||||||
|
modelPresetNameError,
|
||||||
modelPresetPendingDelete,
|
modelPresetPendingDelete,
|
||||||
nanobotFeatureAction,
|
nanobotFeatureAction,
|
||||||
nanobotFeatureConfirm,
|
nanobotFeatureConfirm,
|
||||||
@@ -187,6 +188,7 @@ export function SettingsPage({
|
|||||||
setMcpOAuthCallbackUrl,
|
setMcpOAuthCallbackUrl,
|
||||||
setModelPresetCreating,
|
setModelPresetCreating,
|
||||||
setModelPresetEditingName,
|
setModelPresetEditingName,
|
||||||
|
setModelPresetNameError,
|
||||||
setModelPresetPendingDelete,
|
setModelPresetPendingDelete,
|
||||||
setNanobotFeatureConfirm,
|
setNanobotFeatureConfirm,
|
||||||
setNanobotFeatures,
|
setNanobotFeatures,
|
||||||
@@ -243,6 +245,7 @@ export function SettingsPage({
|
|||||||
form={form}
|
form={form}
|
||||||
setForm={setForm}
|
setForm={setForm}
|
||||||
editingPresetName={modelPresetEditingName}
|
editingPresetName={modelPresetEditingName}
|
||||||
|
presetNameError={modelPresetNameError}
|
||||||
settings={settings}
|
settings={settings}
|
||||||
dirty={modelDirty}
|
dirty={modelDirty}
|
||||||
creating={modelPresetCreating}
|
creating={modelPresetCreating}
|
||||||
@@ -259,9 +262,11 @@ export function SettingsPage({
|
|||||||
onMigrate={handleMigrateModelConfigurations}
|
onMigrate={handleMigrateModelConfigurations}
|
||||||
onBeginCreate={beginModelPresetCreation}
|
onBeginCreate={beginModelPresetCreation}
|
||||||
onCancelCreate={cancelModelPresetCreation}
|
onCancelCreate={cancelModelPresetCreation}
|
||||||
|
onClearPresetNameError={() => setModelPresetNameError(null)}
|
||||||
onSelectConfiguration={(name) => {
|
onSelectConfiguration={(name) => {
|
||||||
setModelPresetCreating(false);
|
setModelPresetCreating(false);
|
||||||
setModelPresetEditingName(name);
|
setModelPresetEditingName(name);
|
||||||
|
setModelPresetNameError(null);
|
||||||
modelPresetBeforeCreateRef.current = null;
|
modelPresetBeforeCreateRef.current = null;
|
||||||
}}
|
}}
|
||||||
onDeleteConfiguration={setModelPresetPendingDelete}
|
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 {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
@@ -160,6 +160,7 @@ export function ModelsSettings({
|
|||||||
form,
|
form,
|
||||||
setForm,
|
setForm,
|
||||||
editingPresetName,
|
editingPresetName,
|
||||||
|
presetNameError,
|
||||||
settings,
|
settings,
|
||||||
dirty,
|
dirty,
|
||||||
creating,
|
creating,
|
||||||
@@ -176,6 +177,7 @@ export function ModelsSettings({
|
|||||||
onMigrate,
|
onMigrate,
|
||||||
onBeginCreate,
|
onBeginCreate,
|
||||||
onCancelCreate,
|
onCancelCreate,
|
||||||
|
onClearPresetNameError,
|
||||||
onSelectConfiguration,
|
onSelectConfiguration,
|
||||||
onDeleteConfiguration,
|
onDeleteConfiguration,
|
||||||
}: {
|
}: {
|
||||||
@@ -183,6 +185,7 @@ export function ModelsSettings({
|
|||||||
form: AgentSettingsDraft;
|
form: AgentSettingsDraft;
|
||||||
setForm: Dispatch<SetStateAction<AgentSettingsDraft>>;
|
setForm: Dispatch<SetStateAction<AgentSettingsDraft>>;
|
||||||
editingPresetName: string;
|
editingPresetName: string;
|
||||||
|
presetNameError: string | null;
|
||||||
settings: SettingsPayload;
|
settings: SettingsPayload;
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
creating: boolean;
|
creating: boolean;
|
||||||
@@ -199,6 +202,7 @@ export function ModelsSettings({
|
|||||||
onMigrate: () => void;
|
onMigrate: () => void;
|
||||||
onBeginCreate: () => void;
|
onBeginCreate: () => void;
|
||||||
onCancelCreate: () => void;
|
onCancelCreate: () => void;
|
||||||
|
onClearPresetNameError: () => void;
|
||||||
onSelectConfiguration: (name: string) => void;
|
onSelectConfiguration: (name: string) => void;
|
||||||
onDeleteConfiguration: (preset: SettingsPayload["model_presets"][number]) => 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>) =>
|
const tx = (key: string, fallback: string, values?: Record<string, unknown>) =>
|
||||||
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
t(key, { defaultValue: fallback, ...(values ?? {}) });
|
||||||
const [editorOpen, setEditorOpen] = useState(false);
|
const [editorOpen, setEditorOpen] = useState(false);
|
||||||
|
const presetNameInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [editorRowKey, setEditorRowKey] = useState<string | null>(null);
|
const [editorRowKey, setEditorRowKey] = useState<string | null>(null);
|
||||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||||
const [draggedCallOrderIndex, setDraggedCallOrderIndex] = useState<number | null>(null);
|
const [draggedCallOrderIndex, setDraggedCallOrderIndex] = useState<number | null>(null);
|
||||||
const [dragOverCallOrderIndex, setDragOverCallOrderIndex] = 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 namedPresets = settings.model_presets.filter((preset) => !preset.is_default);
|
||||||
const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset]));
|
const namedPresetsByName = new Map(namedPresets.map((preset) => [preset.name, preset]));
|
||||||
const unorderedPresets = namedPresets.filter((preset) => !callOrder.includes(preset.name));
|
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.",
|
"Used everywhere, including /model commands. Names must be unique.",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Input
|
<div
|
||||||
autoFocus={creating}
|
className={cn(
|
||||||
aria-label={tx("settings.models.presetName", "Preset name")}
|
"w-[min(280px,70vw)] motion-reduce:animate-none",
|
||||||
value={form.modelPreset}
|
presetNameError && "animate-[preset-name-shake_180ms_ease-in-out]",
|
||||||
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
|
)}
|
||||||
onChange={(event) =>
|
>
|
||||||
setForm((prev) => ({ ...prev, modelPreset: event.target.value }))
|
<Input
|
||||||
}
|
ref={presetNameInputRef}
|
||||||
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
|
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>
|
||||||
<SettingsRow title={t("settings.rows.provider")}>
|
<SettingsRow title={t("settings.rows.provider")}>
|
||||||
<ProviderPicker
|
<ProviderPicker
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import type { ModelSettingsState } from "@/components/settings/models/useModelSettingsState";
|
import type { ModelSettingsState } from "@/components/settings/models/useModelSettingsState";
|
||||||
import { normalizeContextWindowTokens } from "@/components/settings/shared/ModelControls";
|
import { normalizeContextWindowTokens } from "@/components/settings/shared/ModelControls";
|
||||||
import {
|
import {
|
||||||
|
ApiError,
|
||||||
completeProviderOAuth,
|
completeProviderOAuth,
|
||||||
createModelConfiguration,
|
createModelConfiguration,
|
||||||
createProviderSettings,
|
createProviderSettings,
|
||||||
@@ -107,6 +108,7 @@ export function useModelSettingsActions({
|
|||||||
setModelMigrationSaving,
|
setModelMigrationSaving,
|
||||||
setModelPresetCreating,
|
setModelPresetCreating,
|
||||||
setModelPresetEditingName,
|
setModelPresetEditingName,
|
||||||
|
setModelPresetNameError,
|
||||||
setModelPresetPendingDelete,
|
setModelPresetPendingDelete,
|
||||||
setProviderForms,
|
setProviderForms,
|
||||||
setProviderOAuthCompleting,
|
setProviderOAuthCompleting,
|
||||||
@@ -119,6 +121,33 @@ export function useModelSettingsActions({
|
|||||||
visibleProviderKeys,
|
visibleProviderKeys,
|
||||||
} = state;
|
} = 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 () => {
|
const saveModelSettings = async () => {
|
||||||
if (
|
if (
|
||||||
!settings ||
|
!settings ||
|
||||||
@@ -144,6 +173,11 @@ export function useModelSettingsActions({
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (presetNameConflict(name)) {
|
||||||
|
showPresetNameConflict();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModelPresetNameError(null);
|
||||||
setModelConfigurationSaving(true);
|
setModelConfigurationSaving(true);
|
||||||
try {
|
try {
|
||||||
const payload = await createModelConfiguration(client, {
|
const payload = await createModelConfiguration(client, {
|
||||||
@@ -175,9 +209,10 @@ export function useModelSettingsActions({
|
|||||||
}
|
}
|
||||||
modelPresetBeforeCreateRef.current = null;
|
modelPresetBeforeCreateRef.current = null;
|
||||||
onModelNameChange(finalPayload.agent.model || null);
|
onModelNameChange(finalPayload.agent.model || null);
|
||||||
|
setModelPresetNameError(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError((err as Error).message);
|
handlePresetSaveError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setModelConfigurationSaving(false);
|
setModelConfigurationSaving(false);
|
||||||
}
|
}
|
||||||
@@ -190,6 +225,11 @@ export function useModelSettingsActions({
|
|||||||
);
|
);
|
||||||
if (!selectedPreset) return;
|
if (!selectedPreset) return;
|
||||||
const nextName = form.modelPreset.trim();
|
const nextName = form.modelPreset.trim();
|
||||||
|
if (presetNameConflict(nextName, selectedPreset.name)) {
|
||||||
|
showPresetNameConflict();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setModelPresetNameError(null);
|
||||||
const reasoningEffort = form.reasoningEffort || null;
|
const reasoningEffort = form.reasoningEffort || null;
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
try {
|
try {
|
||||||
@@ -214,9 +254,10 @@ export function useModelSettingsActions({
|
|||||||
setForm(agentDraftFromPayload(payload, nextName));
|
setForm(agentDraftFromPayload(payload, nextName));
|
||||||
setModelPresetEditingName(nextName);
|
setModelPresetEditingName(nextName);
|
||||||
onModelNameChange(payload.agent.model || null);
|
onModelNameChange(payload.agent.model || null);
|
||||||
|
setModelPresetNameError(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError((err as Error).message);
|
handlePresetSaveError(err);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -235,6 +276,7 @@ export function useModelSettingsActions({
|
|||||||
configuredModelProviderOptions[0]?.name ??
|
configuredModelProviderOptions[0]?.name ??
|
||||||
"";
|
"";
|
||||||
modelPresetBeforeCreateRef.current = modelPresetEditingName;
|
modelPresetBeforeCreateRef.current = modelPresetEditingName;
|
||||||
|
setModelPresetNameError(null);
|
||||||
setForm((prev) => ({
|
setForm((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
modelPreset: "",
|
modelPreset: "",
|
||||||
@@ -254,6 +296,7 @@ export function useModelSettingsActions({
|
|||||||
if (!settings || modelConfigurationSaving) return;
|
if (!settings || modelConfigurationSaving) return;
|
||||||
const previousPreset = modelPresetBeforeCreateRef.current;
|
const previousPreset = modelPresetBeforeCreateRef.current;
|
||||||
setModelPresetCreating(false);
|
setModelPresetCreating(false);
|
||||||
|
setModelPresetNameError(null);
|
||||||
setForm(agentDraftFromPayload(settings, previousPreset ?? undefined));
|
setForm(agentDraftFromPayload(settings, previousPreset ?? undefined));
|
||||||
setModelPresetEditingName(previousPreset ?? agentDraftFromPayload(settings).modelPreset);
|
setModelPresetEditingName(previousPreset ?? agentDraftFromPayload(settings).modelPreset);
|
||||||
modelPresetBeforeCreateRef.current = null;
|
modelPresetBeforeCreateRef.current = null;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
|||||||
: DEFAULT_AGENT_SETTINGS_DRAFT;
|
: DEFAULT_AGENT_SETTINGS_DRAFT;
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [modelPresetCreating, setModelPresetCreating] = useState(false);
|
const [modelPresetCreating, setModelPresetCreating] = useState(false);
|
||||||
|
const [modelPresetNameError, setModelPresetNameError] = useState<string | null>(null);
|
||||||
const [modelConfigurationSaving, setModelConfigurationSaving] = useState(false);
|
const [modelConfigurationSaving, setModelConfigurationSaving] = useState(false);
|
||||||
const [modelCallOrderSaving, setModelCallOrderSaving] = useState(false);
|
const [modelCallOrderSaving, setModelCallOrderSaving] = useState(false);
|
||||||
const [modelMigrationSaving, setModelMigrationSaving] = useState(false);
|
const [modelMigrationSaving, setModelMigrationSaving] = useState(false);
|
||||||
@@ -50,6 +51,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
|||||||
modelPresetBeforeCreateRef,
|
modelPresetBeforeCreateRef,
|
||||||
modelPresetCreating,
|
modelPresetCreating,
|
||||||
modelPresetEditingName,
|
modelPresetEditingName,
|
||||||
|
modelPresetNameError,
|
||||||
modelPresetPendingDelete,
|
modelPresetPendingDelete,
|
||||||
providerForms,
|
providerForms,
|
||||||
providerOAuthCompleting,
|
providerOAuthCompleting,
|
||||||
@@ -68,6 +70,7 @@ export function useModelSettingsState(initialSettings: SettingsPayload | null) {
|
|||||||
setModelMigrationSaving,
|
setModelMigrationSaving,
|
||||||
setModelPresetCreating,
|
setModelPresetCreating,
|
||||||
setModelPresetEditingName,
|
setModelPresetEditingName,
|
||||||
|
setModelPresetNameError,
|
||||||
setModelPresetPendingDelete,
|
setModelPresetPendingDelete,
|
||||||
setProviderForms,
|
setProviderForms,
|
||||||
setProviderOAuthCompleting,
|
setProviderOAuthCompleting,
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ export function useSettingsController({
|
|||||||
const {
|
const {
|
||||||
editingProviderKeys, expandedProvider, form, modelCallOrder, modelCallOrderSaving,
|
editingProviderKeys, expandedProvider, form, modelCallOrder, modelCallOrderSaving,
|
||||||
modelConfigurationSaving, modelMigrationSaving, modelPresetBeforeCreateRef,
|
modelConfigurationSaving, modelMigrationSaving, modelPresetBeforeCreateRef,
|
||||||
modelPresetCreating, modelPresetEditingName, modelPresetPendingDelete,
|
modelPresetCreating, modelPresetEditingName, modelPresetNameError, modelPresetPendingDelete,
|
||||||
providerForms, providerOAuthCompleting,
|
providerForms, providerOAuthCompleting,
|
||||||
providerOAuthDialogError, providerOAuthFlow, providerOAuthFlowRef, providerOAuthResponse,
|
providerOAuthDialogError, providerOAuthFlow, providerOAuthFlowRef, providerOAuthResponse,
|
||||||
providerSaving, saving, setForm,
|
providerSaving, saving, setForm,
|
||||||
setModelCallOrder, setModelPresetCreating, setModelPresetEditingName,
|
setModelCallOrder, setModelPresetCreating, setModelPresetEditingName, setModelPresetNameError,
|
||||||
setModelPresetPendingDelete,
|
setModelPresetPendingDelete,
|
||||||
setProviderForms, setProviderOAuthCompleting, setProviderOAuthDialogError,
|
setProviderForms, setProviderOAuthCompleting, setProviderOAuthDialogError,
|
||||||
setProviderOAuthFlow, setProviderOAuthResponse, visibleProviderKeys,
|
setProviderOAuthFlow, setProviderOAuthResponse, visibleProviderKeys,
|
||||||
@@ -538,6 +538,7 @@ export function useSettingsController({
|
|||||||
modelPresetBeforeCreateRef,
|
modelPresetBeforeCreateRef,
|
||||||
modelPresetCreating,
|
modelPresetCreating,
|
||||||
modelPresetEditingName,
|
modelPresetEditingName,
|
||||||
|
modelPresetNameError,
|
||||||
modelPresetPendingDelete,
|
modelPresetPendingDelete,
|
||||||
nanobotFeatureAction,
|
nanobotFeatureAction,
|
||||||
nanobotFeatureConfirm,
|
nanobotFeatureConfirm,
|
||||||
@@ -588,6 +589,7 @@ export function useSettingsController({
|
|||||||
setMcpOAuthCallbackUrl,
|
setMcpOAuthCallbackUrl,
|
||||||
setModelPresetCreating,
|
setModelPresetCreating,
|
||||||
setModelPresetEditingName,
|
setModelPresetEditingName,
|
||||||
|
setModelPresetNameError,
|
||||||
setModelPresetPendingDelete,
|
setModelPresetPendingDelete,
|
||||||
setNanobotFeatureConfirm,
|
setNanobotFeatureConfirm,
|
||||||
setNanobotFeatures,
|
setNanobotFeatures,
|
||||||
|
|||||||
@@ -696,6 +696,21 @@
|
|||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes preset-name-shake {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
35% {
|
||||||
|
transform: translateX(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
transform: translateX(2px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes composer-model-pill-viewport-enter {
|
@keyframes composer-model-pill-viewport-enter {
|
||||||
from {
|
from {
|
||||||
transform: scale(0.9074);
|
transform: scale(0.9074);
|
||||||
|
|||||||
@@ -144,6 +144,7 @@
|
|||||||
"editPreset": "Edit preset",
|
"editPreset": "Edit preset",
|
||||||
"presetName": "Preset name",
|
"presetName": "Preset name",
|
||||||
"presetNameHelp": "Used in the interface and /model commands. Names must be unique.",
|
"presetNameHelp": "Used in the interface and /model commands. Names must be unique.",
|
||||||
|
"presetNameDuplicate": "A preset with this name already exists.",
|
||||||
"presetNamePlaceholder": "Fast writing",
|
"presetNamePlaceholder": "Fast writing",
|
||||||
"advancedOptions": "Advanced options",
|
"advancedOptions": "Advanced options",
|
||||||
"advancedSummary": "Context {{context}} · Max {{max}} tokens",
|
"advancedSummary": "Context {{context}} · Max {{max}} tokens",
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
"editPreset": "Editar preajuste",
|
"editPreset": "Editar preajuste",
|
||||||
"presetName": "Nombre del preajuste",
|
"presetName": "Nombre del preajuste",
|
||||||
"presetNameHelp": "Se usa en la interfaz y en /model. Los nombres deben ser únicos.",
|
"presetNameHelp": "Se usa en la interfaz y en /model. Los nombres deben ser únicos.",
|
||||||
|
"presetNameDuplicate": "Ya existe un preajuste con este nombre.",
|
||||||
"presetNamePlaceholder": "Escritura rápida",
|
"presetNamePlaceholder": "Escritura rápida",
|
||||||
"advancedOptions": "Opciones avanzadas",
|
"advancedOptions": "Opciones avanzadas",
|
||||||
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
"editPreset": "Modifier le préréglage",
|
"editPreset": "Modifier le préréglage",
|
||||||
"presetName": "Nom du préréglage",
|
"presetName": "Nom du préréglage",
|
||||||
"presetNameHelp": "Utilisé dans l’interface et avec /model. Les noms doivent être uniques.",
|
"presetNameHelp": "Utilisé dans l’interface et avec /model. Les noms doivent être uniques.",
|
||||||
|
"presetNameDuplicate": "Un préréglage portant ce nom existe déjà.",
|
||||||
"presetNamePlaceholder": "Rédaction rapide",
|
"presetNamePlaceholder": "Rédaction rapide",
|
||||||
"advancedOptions": "Options avancées",
|
"advancedOptions": "Options avancées",
|
||||||
"advancedSummary": "Contexte {{context}} · Max. {{max}} tokens",
|
"advancedSummary": "Contexte {{context}} · Max. {{max}} tokens",
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
"editPreset": "Ubah prasetel",
|
"editPreset": "Ubah prasetel",
|
||||||
"presetName": "Nama prasetel",
|
"presetName": "Nama prasetel",
|
||||||
"presetNameHelp": "Digunakan di antarmuka dan perintah /model. Nama harus unik.",
|
"presetNameHelp": "Digunakan di antarmuka dan perintah /model. Nama harus unik.",
|
||||||
|
"presetNameDuplicate": "Preset dengan nama ini sudah ada.",
|
||||||
"presetNamePlaceholder": "Menulis cepat",
|
"presetNamePlaceholder": "Menulis cepat",
|
||||||
"advancedOptions": "Opsi lanjutan",
|
"advancedOptions": "Opsi lanjutan",
|
||||||
"advancedSummary": "Konteks {{context}} · Maks. {{max}} token",
|
"advancedSummary": "Konteks {{context}} · Maks. {{max}} token",
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
"editPreset": "プリセットを編集",
|
"editPreset": "プリセットを編集",
|
||||||
"presetName": "プリセット名",
|
"presetName": "プリセット名",
|
||||||
"presetNameHelp": "画面と /model コマンドで使用します。名前は一意である必要があります。",
|
"presetNameHelp": "画面と /model コマンドで使用します。名前は一意である必要があります。",
|
||||||
|
"presetNameDuplicate": "同じ名前のプリセットがすでに存在します。",
|
||||||
"presetNamePlaceholder": "高速執筆",
|
"presetNamePlaceholder": "高速執筆",
|
||||||
"advancedOptions": "詳細オプション",
|
"advancedOptions": "詳細オプション",
|
||||||
"advancedSummary": "コンテキスト {{context}} · 最大 {{max}} トークン",
|
"advancedSummary": "コンテキスト {{context}} · 最大 {{max}} トークン",
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
"editPreset": "프리셋 편집",
|
"editPreset": "프리셋 편집",
|
||||||
"presetName": "프리셋 이름",
|
"presetName": "프리셋 이름",
|
||||||
"presetNameHelp": "화면과 /model 명령에서 사용됩니다. 이름은 고유해야 합니다.",
|
"presetNameHelp": "화면과 /model 명령에서 사용됩니다. 이름은 고유해야 합니다.",
|
||||||
|
"presetNameDuplicate": "같은 이름의 프리셋이 이미 있습니다.",
|
||||||
"presetNamePlaceholder": "빠른 작성",
|
"presetNamePlaceholder": "빠른 작성",
|
||||||
"advancedOptions": "고급 옵션",
|
"advancedOptions": "고급 옵션",
|
||||||
"advancedSummary": "컨텍스트 {{context}} · 최대 {{max}} 토큰",
|
"advancedSummary": "컨텍스트 {{context}} · 최대 {{max}} 토큰",
|
||||||
|
|||||||
@@ -144,6 +144,7 @@
|
|||||||
"editPreset": "Editar predefinição",
|
"editPreset": "Editar predefinição",
|
||||||
"presetName": "Nome da predefinição",
|
"presetName": "Nome da predefinição",
|
||||||
"presetNameHelp": "Usado na interface e nos comandos /model. Os nomes devem ser únicos.",
|
"presetNameHelp": "Usado na interface e nos comandos /model. Os nomes devem ser únicos.",
|
||||||
|
"presetNameDuplicate": "Já existe uma predefinição com este nome.",
|
||||||
"presetNamePlaceholder": "Escrita rápida",
|
"presetNamePlaceholder": "Escrita rápida",
|
||||||
"advancedOptions": "Opções avançadas",
|
"advancedOptions": "Opções avançadas",
|
||||||
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
"advancedSummary": "Contexto {{context}} · Máx. {{max}} tokens",
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
"editPreset": "Sửa cấu hình đặt trước",
|
"editPreset": "Sửa cấu hình đặt trước",
|
||||||
"presetName": "Tên cấu hình đặt trước",
|
"presetName": "Tên cấu hình đặt trước",
|
||||||
"presetNameHelp": "Dùng trong giao diện và lệnh /model. Tên phải là duy nhất.",
|
"presetNameHelp": "Dùng trong giao diện và lệnh /model. Tên phải là duy nhất.",
|
||||||
|
"presetNameDuplicate": "Đã có một cấu hình sẵn với tên này.",
|
||||||
"presetNamePlaceholder": "Viết nhanh",
|
"presetNamePlaceholder": "Viết nhanh",
|
||||||
"advancedOptions": "Tùy chọn nâng cao",
|
"advancedOptions": "Tùy chọn nâng cao",
|
||||||
"advancedSummary": "Ngữ cảnh {{context}} · Tối đa {{max}} token",
|
"advancedSummary": "Ngữ cảnh {{context}} · Tối đa {{max}} token",
|
||||||
|
|||||||
@@ -144,6 +144,7 @@
|
|||||||
"editPreset": "编辑预设",
|
"editPreset": "编辑预设",
|
||||||
"presetName": "预设名称",
|
"presetName": "预设名称",
|
||||||
"presetNameHelp": "用于界面和 /model 命令;名称不能重复。",
|
"presetNameHelp": "用于界面和 /model 命令;名称不能重复。",
|
||||||
|
"presetNameDuplicate": "已存在同名预设。",
|
||||||
"presetNamePlaceholder": "快速写作",
|
"presetNamePlaceholder": "快速写作",
|
||||||
"advancedOptions": "高级选项",
|
"advancedOptions": "高级选项",
|
||||||
"advancedSummary": "上下文 {{context}} · 最大输出 {{max}} tokens",
|
"advancedSummary": "上下文 {{context}} · 最大输出 {{max}} tokens",
|
||||||
|
|||||||
@@ -420,6 +420,7 @@
|
|||||||
"editPreset": "編輯預設",
|
"editPreset": "編輯預設",
|
||||||
"presetName": "預設名稱",
|
"presetName": "預設名稱",
|
||||||
"presetNameHelp": "用於介面和 /model 命令;名稱不得重複。",
|
"presetNameHelp": "用於介面和 /model 命令;名稱不得重複。",
|
||||||
|
"presetNameDuplicate": "已有同名預設。",
|
||||||
"presetNamePlaceholder": "快速寫作",
|
"presetNamePlaceholder": "快速寫作",
|
||||||
"advancedOptions": "進階選項",
|
"advancedOptions": "進階選項",
|
||||||
"advancedSummary": "上下文 {{context}} · 最大輸出 {{max}} tokens",
|
"advancedSummary": "上下文 {{context}} · 最大輸出 {{max}} tokens",
|
||||||
|
|||||||
@@ -145,6 +145,25 @@ describe("Settings models", () => {
|
|||||||
expect(screen.getByRole("textbox", { name: "Preset name" })).toHaveValue("Codex");
|
expect(screen.getByRole("textbox", { name: "Preset name" })).toHaveValue("Codex");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("maps a server-side name conflict back to the preset name field", async () => {
|
||||||
|
const payload = settingsPayload();
|
||||||
|
vi.stubGlobal("fetch", vi.fn(() => new Promise<Response>(() => {})));
|
||||||
|
requestMutationMock.mockRejectedValueOnce({ status: 409 });
|
||||||
|
|
||||||
|
renderSettingsView({ initialSection: "models", initialSettings: payload });
|
||||||
|
await togglePresetEditor();
|
||||||
|
|
||||||
|
const nameInput = screen.getByRole("textbox", { name: "Preset name" });
|
||||||
|
fireEvent.change(nameInput, { target: { value: "Codex" } });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save preset" }));
|
||||||
|
|
||||||
|
expect(await screen.findByRole("alert")).toHaveTextContent(
|
||||||
|
"A preset with this name already exists.",
|
||||||
|
);
|
||||||
|
expect(nameInput).toHaveAttribute("aria-invalid", "true");
|
||||||
|
expect(nameInput).toHaveFocus();
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps generation parameters collapsed until advanced options are opened", async () => {
|
it("keeps generation parameters collapsed until advanced options are opened", async () => {
|
||||||
vi.stubGlobal(
|
vi.stubGlobal(
|
||||||
"fetch",
|
"fetch",
|
||||||
@@ -578,6 +597,41 @@ describe("Settings models", () => {
|
|||||||
expect(screen.queryByRole("button", { name: "Save order" })).not.toBeInTheDocument();
|
expect(screen.queryByRole("button", { name: "Save order" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("shows an inline error when a new preset name already exists", async () => {
|
||||||
|
const { payload } = settingsPayloadWithBackup();
|
||||||
|
vi.stubGlobal(
|
||||||
|
"fetch",
|
||||||
|
vi.fn(async () => ({ ok: false, status: 404, json: async () => ({}) }) as Response),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderSettingsView({ initialSection: "models", initialSettings: payload });
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "New model preset" }));
|
||||||
|
const nameInput = screen.getByRole("textbox", { name: "Preset name" });
|
||||||
|
fireEvent.change(nameInput, { target: { value: "PRIMARY" } });
|
||||||
|
await openPopover(screen.getByRole("button", { name: "Select model" }));
|
||||||
|
const modelSearch = await screen.findByRole("combobox", {
|
||||||
|
name: "Search or type model ID",
|
||||||
|
});
|
||||||
|
fireEvent.change(modelSearch, { target: { value: "openai/gpt-4o-mini" } });
|
||||||
|
fireEvent.keyDown(modelSearch, { key: "Enter" });
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: "Save preset" }));
|
||||||
|
|
||||||
|
expect(requestMutationMock).not.toHaveBeenCalled();
|
||||||
|
expect(nameInput).toHaveAttribute("aria-invalid", "true");
|
||||||
|
expect(nameInput).toHaveAttribute("aria-describedby", "model-preset-name-error");
|
||||||
|
expect(screen.getByRole("alert")).toHaveTextContent(
|
||||||
|
"A preset with this name already exists.",
|
||||||
|
);
|
||||||
|
expect(nameInput.parentElement).toHaveClass(
|
||||||
|
"animate-[preset-name-shake_180ms_ease-in-out]",
|
||||||
|
);
|
||||||
|
|
||||||
|
fireEvent.change(nameInput, { target: { value: "Writer" } });
|
||||||
|
expect(nameInput).toHaveAttribute("aria-invalid", "false");
|
||||||
|
expect(screen.queryByRole("alert")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
it("converts legacy model settings into presets before editing call order", async () => {
|
it("converts legacy model settings into presets before editing call order", async () => {
|
||||||
const migratedPayload = settingsPayload();
|
const migratedPayload = settingsPayload();
|
||||||
const defaultPreset = {
|
const defaultPreset = {
|
||||||
|
|||||||
Reference in New Issue
Block a user