refactor(webui): expand model preset editor inline

This commit is contained in:
Xubin Ren
2026-08-07 12:42:37 +08:00
parent ada07aa799
commit e971e81b6c
12 changed files with 198 additions and 277 deletions
+186 -232
View File
@@ -3375,9 +3375,10 @@ function ModelsSettings({
); );
const callOrderBusy = orderSaving || saving; const callOrderBusy = orderSaving || saving;
const selectPreset = (preset: SettingsPayload["model_presets"][number]) => { const selectPreset = (preset: SettingsPayload["model_presets"][number]) => {
const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name;
onSelectConfiguration(); onSelectConfiguration();
if (!creating && selectedPreset?.name === preset.name) { if (toggleCurrentPreset) {
setEditorOpen(true); setEditorOpen((open) => !open);
return; return;
} }
setForm((prev) => ({ setForm((prev) => ({
@@ -3393,11 +3394,6 @@ function ModelsSettings({
})); }));
setEditorOpen(true); setEditorOpen(true);
}; };
const closeEditor = () => {
if (creatingSaving) return;
if (creating) onCancelCreate();
setEditorOpen(false);
};
const moveCallOrderItem = (index: number, offset: -1 | 1) => { const moveCallOrderItem = (index: number, offset: -1 | 1) => {
if (callOrderBusy) return; if (callOrderBusy) return;
@@ -3436,6 +3432,177 @@ function ModelsSettings({
onChangeCallOrder(next); onChangeCallOrder(next);
}; };
const renderPresetEditor = (key: string) => (
<div
key={key}
id="model-preset-editor"
data-testid="model-preset-editor"
className="divide-y divide-border/45 bg-muted/10 motion-reduce:animate-none animate-in fade-in-0 slide-in-from-top-1 duration-200"
>
{creating ? (
<div className="flex min-h-[52px] items-center px-4 py-3 sm:px-5">
<span className="text-[13px] font-semibold text-foreground/85">
{tx("settings.models.newPreset", "New model preset")}
</span>
</div>
) : null}
<SettingsRow title={tx("settings.models.presetName", "Preset name")}>
<Input
autoFocus={creating}
value={form.presetLabel}
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
onChange={(event) =>
setForm((prev) => ({ ...prev, presetLabel: event.target.value }))
}
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
/>
</SettingsRow>
<SettingsRow title={t("settings.rows.provider")}>
<ProviderPicker
providers={providerOptions}
value={providerValue}
emptyLabel={t("settings.byok.noConfiguredProviders")}
showProviderLogos={showBrandLogos}
onChange={(provider) =>
setForm((prev) => ({
...prev,
provider,
model: provider === prev.provider ? prev.model : "",
}))
}
/>
</SettingsRow>
{selectedProviderNeedsSignIn ? (
<SettingsRow
title={tx("settings.oauth.signInRequired", "Sign in required")}
description={tx(
"settings.oauth.signInBeforeSaving",
"Sign in before saving this provider in the preset.",
)}
>
<Button
size="sm"
variant="outline"
onClick={() => selectedProvider && onProviderOAuthLogin(selectedProvider.name)}
disabled={!selectedProvider?.oauth_login_supported || selectedProviderSigningIn}
className="rounded-full"
>
{selectedProviderSigningIn ? (
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
) : null}
{selectedProviderSigningIn
? tx("settings.oauth.signingIn", "Signing in...")
: tx("settings.oauth.signIn", "Sign in")}
</Button>
</SettingsRow>
) : null}
<SettingsRow title={t("settings.rows.model")}>
<ModelIdPicker
token={token}
settings={settings}
provider={form.provider}
value={form.model}
showProviderLogos={showBrandLogos}
onChange={(model) => setForm((prev) => ({ ...prev, model }))}
/>
</SettingsRow>
<button
type="button"
aria-expanded={advancedOpen}
onClick={() => setAdvancedOpen((value) => !value)}
className="flex min-h-[62px] w-full items-center justify-between gap-4 px-4 py-3.5 text-left transition-colors hover:bg-muted/30 sm:px-5"
>
<span>
<span className="block text-[14px] font-medium text-foreground">
{tx("settings.models.advancedOptions", "Advanced options")}
</span>
<span className="mt-0.5 block text-[12px] text-muted-foreground">
{tx(
"settings.models.advancedSummary",
"Context {{context}} · Max {{max}} tokens",
{
context: formatModelContextWindow(form.contextWindowTokens),
max: formatContextWindow(form.maxTokens),
},
)}
</span>
</span>
<ChevronDown
className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
advancedOpen && "rotate-180",
)}
aria-hidden
/>
</button>
{advancedOpen ? (
<div className="bg-muted/12 px-4 py-4 sm:px-5">
<ModelAdvancedFields
maxTokens={form.maxTokens}
contextWindowTokens={form.contextWindowTokens}
temperature={form.temperature}
reasoningEffort={form.reasoningEffort}
onChange={(value) => setForm((prev) => ({ ...prev, ...value }))}
/>
</div>
) : null}
<div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
{creating ? (
<Button
size="sm"
variant="ghost"
className="self-start rounded-full text-muted-foreground"
disabled={creatingSaving}
onClick={() => {
setEditorOpen(false);
onCancelCreate();
}}
>
{tx("settings.actions.cancel", "Cancel")}
</Button>
) : selectedPreset ? (
<Button
size="sm"
variant="ghost"
className="self-start rounded-full text-muted-foreground hover:text-destructive"
disabled={selectedPresetReferenced || saving || orderSaving}
title={
selectedPresetReferenced
? tx(
"settings.models.removeBeforeDelete",
"Remove this preset from the call order before deleting it.",
)
: undefined
}
onClick={() => onDeleteConfiguration(selectedPreset)}
>
<Trash2 className="mr-1.5 h-3.5 w-3.5" aria-hidden />
{tx("settings.actions.delete", "Delete")}
</Button>
) : null}
<div className="flex items-center justify-end gap-3">
<Button
size="sm"
variant="outline"
className="rounded-full"
disabled={
(!creating && !dirty) ||
!selectedProviderConfigured ||
modelFieldsMissing ||
saving ||
orderSaving
}
onClick={onSave}
>
{saving || creatingSaving
? tx("settings.actions.saving", "Saving...")
: tx("settings.actions.savePreset", "Save preset")}
</Button>
</div>
</div>
</div>
);
return ( return (
<div className="space-y-7"> <div className="space-y-7">
<section> <section>
@@ -3477,13 +3644,7 @@ function ModelsSettings({
</Button> </Button>
</div> </div>
) : ( ) : (
<div className="xl:grid xl:grid-cols-[minmax(0,5fr)_minmax(0,6fr)] xl:divide-x xl:divide-border/45"> <>
<div
className={cn(
"min-w-0 divide-y divide-border/45",
editorOpen && "hidden xl:block",
)}
>
<div role="list" className="divide-y divide-border/45"> <div role="list" className="divide-y divide-border/45">
{presetRows.map(({ key, name, orderIndex, preset }) => { {presetRows.map(({ key, name, orderIndex, preset }) => {
const ordered = orderIndex >= 0; const ordered = orderIndex >= 0;
@@ -3507,7 +3668,7 @@ function ModelsSettings({
draggedCallOrderIndex < orderIndex; draggedCallOrderIndex < orderIndex;
const isSelected = const isSelected =
editorOpen && !creating && selectedPreset?.name === name; editorOpen && !creating && selectedPreset?.name === name;
return ( return [
<div <div
key={key} key={key}
role="listitem" role="listitem"
@@ -3593,8 +3754,9 @@ function ModelsSettings({
)} )}
<button <button
type="button" type="button"
aria-pressed={isSelected} aria-pressed={selectedPreset?.name === name}
aria-controls="model-preset-detail" aria-expanded={isSelected}
aria-controls={isSelected ? "model-preset-editor" : undefined}
disabled={!preset} disabled={!preset}
onClick={() => preset && selectPreset(preset)} onClick={() => preset && selectPreset(preset)}
className="flex min-w-0 flex-1 items-center gap-3 rounded-[12px] text-left outline-none focus-visible:ring-2 focus-visible:ring-ring" className="flex min-w-0 flex-1 items-center gap-3 rounded-[12px] text-left outline-none focus-visible:ring-2 focus-visible:ring-ring"
@@ -3640,8 +3802,8 @@ function ModelsSettings({
</span> </span>
<ChevronRight <ChevronRight
className={cn( className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-[color,transform] group-hover:translate-x-0.5", "h-4 w-4 shrink-0 text-muted-foreground transition-transform",
isSelected && "text-foreground", isSelected && "rotate-90",
)} )}
aria-hidden aria-hidden
/> />
@@ -3676,8 +3838,9 @@ function ModelsSettings({
aria-hidden aria-hidden
/> />
</button> </button>
</div> </div>,
); isSelected ? renderPresetEditor(`editor:${key}`) : null,
];
})} })}
</div> </div>
<div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5"> <div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
@@ -3707,217 +3870,8 @@ function ModelsSettings({
</SettingsStatusMessage> </SettingsStatusMessage>
) : null} ) : null}
</div> </div>
</div> {creating && editorOpen ? renderPresetEditor("editor:new") : null}
<div </>
id="model-preset-detail"
className={cn(
"min-w-0 divide-y divide-border/45",
!editorOpen && "hidden xl:block",
)}
>
{editorOpen && (selectedPreset || creating) ? (
<>
<div className="flex min-h-[64px] items-center gap-2 bg-muted/15 px-4 py-3 sm:px-5">
<button
type="button"
onClick={closeEditor}
disabled={creatingSaving}
aria-label={tx(
"settings.models.backToPresets",
"Back to model presets",
)}
className="-ml-2 grid h-8 w-8 shrink-0 place-items-center rounded-full text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-40 xl:hidden"
>
<ChevronLeft className="h-4 w-4" aria-hidden />
</button>
<span className="min-w-0">
<span className="block truncate text-[14px] font-semibold text-foreground/90">
{creating
? tx("settings.models.newPreset", "New model preset")
: selectedPreset?.label}
</span>
<span className="mt-0.5 block truncate text-[12px] text-muted-foreground">
{creating
? tx(
"settings.models.newPresetHelp",
"Save a reusable model and its generation settings.",
)
: tx("settings.models.editPreset", "Edit preset")}
</span>
</span>
</div>
<SettingsRow title={tx("settings.models.presetName", "Preset name")}>
<Input
autoFocus={creating}
value={form.presetLabel}
placeholder={tx("settings.models.presetNamePlaceholder", "Fast writing")}
onChange={(event) =>
setForm((prev) => ({ ...prev, presetLabel: event.target.value }))
}
className="h-8 w-[min(280px,70vw)] rounded-full text-[13px]"
/>
</SettingsRow>
<SettingsRow title={t("settings.rows.provider")}>
<ProviderPicker
providers={providerOptions}
value={providerValue}
emptyLabel={t("settings.byok.noConfiguredProviders")}
showProviderLogos={showBrandLogos}
onChange={(provider) =>
setForm((prev) => ({
...prev,
provider,
model: provider === prev.provider ? prev.model : "",
}))
}
/>
</SettingsRow>
{selectedProviderNeedsSignIn ? (
<SettingsRow
title={tx("settings.oauth.signInRequired", "Sign in required")}
description={tx(
"settings.oauth.signInBeforeSaving",
"Sign in before saving this provider in the preset.",
)}
>
<Button
size="sm"
variant="outline"
onClick={() => selectedProvider && onProviderOAuthLogin(selectedProvider.name)}
disabled={!selectedProvider?.oauth_login_supported || selectedProviderSigningIn}
className="rounded-full"
>
{selectedProviderSigningIn ? (
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
) : null}
{selectedProviderSigningIn
? tx("settings.oauth.signingIn", "Signing in...")
: tx("settings.oauth.signIn", "Sign in")}
</Button>
</SettingsRow>
) : null}
<SettingsRow title={t("settings.rows.model")}>
<ModelIdPicker
token={token}
settings={settings}
provider={form.provider}
value={form.model}
showProviderLogos={showBrandLogos}
onChange={(model) => setForm((prev) => ({ ...prev, model }))}
/>
</SettingsRow>
<button
type="button"
aria-expanded={advancedOpen}
onClick={() => setAdvancedOpen((value) => !value)}
className="flex min-h-[62px] w-full items-center justify-between gap-4 px-4 py-3.5 text-left transition-colors hover:bg-muted/30 sm:px-5"
>
<span>
<span className="block text-[14px] font-medium text-foreground">
{tx("settings.models.advancedOptions", "Advanced options")}
</span>
<span className="mt-0.5 block text-[12px] text-muted-foreground">
{tx(
"settings.models.advancedSummary",
"Context {{context}} · Max {{max}} tokens",
{
context: formatModelContextWindow(form.contextWindowTokens),
max: formatContextWindow(form.maxTokens),
},
)}
</span>
</span>
<ChevronDown
className={cn(
"h-4 w-4 shrink-0 text-muted-foreground transition-transform",
advancedOpen && "rotate-180",
)}
aria-hidden
/>
</button>
{advancedOpen ? (
<div className="bg-muted/12 px-4 py-4 sm:px-5">
<ModelAdvancedFields
maxTokens={form.maxTokens}
contextWindowTokens={form.contextWindowTokens}
temperature={form.temperature}
reasoningEffort={form.reasoningEffort}
onChange={(value) => setForm((prev) => ({ ...prev, ...value }))}
/>
</div>
) : null}
<div className="flex min-h-[58px] flex-col gap-3 px-4 py-3 sm:flex-row sm:items-center sm:justify-between sm:px-5">
{creating ? (
<Button
size="sm"
variant="ghost"
className="self-start rounded-full text-muted-foreground"
disabled={creatingSaving}
onClick={closeEditor}
>
{tx("settings.actions.cancel", "Cancel")}
</Button>
) : selectedPreset ? (
<Button
size="sm"
variant="ghost"
className="self-start rounded-full text-muted-foreground hover:text-destructive"
disabled={selectedPresetReferenced || saving || orderSaving}
title={
selectedPresetReferenced
? tx(
"settings.models.removeBeforeDelete",
"Remove this preset from the call order before deleting it.",
)
: undefined
}
onClick={() => onDeleteConfiguration(selectedPreset)}
>
<Trash2 className="mr-1.5 h-3.5 w-3.5" aria-hidden />
{tx("settings.actions.delete", "Delete")}
</Button>
) : null}
<div className="flex items-center justify-end gap-3">
<Button
size="sm"
variant="outline"
className="rounded-full"
disabled={
(!creating && !dirty) ||
!selectedProviderConfigured ||
modelFieldsMissing ||
saving ||
orderSaving
}
onClick={onSave}
>
{saving || creatingSaving
? tx("settings.actions.saving", "Saving...")
: tx("settings.actions.savePreset", "Save preset")}
</Button>
</div>
</div>
</>
) : (
<div className="flex min-h-[320px] items-center justify-center px-8 py-12 text-center">
<div className="max-w-[16rem]">
<span className="mx-auto grid h-10 w-10 place-items-center rounded-[14px] bg-muted text-muted-foreground">
<Pencil className="h-4 w-4" aria-hidden />
</span>
<p className="mt-3 text-[14px] font-medium text-foreground">
{tx("settings.models.selectPreset", "Select a preset")}
</p>
<p className="mt-1 text-[12px] leading-5 text-muted-foreground">
{tx(
"settings.models.selectPresetHelp",
"Select a preset from the list to edit its model and generation settings.",
)}
</p>
</div>
</div>
)}
</div>
</div>
)} )}
</SettingsGroup> </SettingsGroup>
</section> </section>
-3
View File
@@ -132,9 +132,6 @@
"newPresetHelp": "Save a reusable model and its generation settings.", "newPresetHelp": "Save a reusable model and its generation settings.",
"presets": "Model presets", "presets": "Model presets",
"editPreset": "Edit preset", "editPreset": "Edit preset",
"backToPresets": "Back to model presets",
"selectPreset": "Select a preset",
"selectPresetHelp": "Select a preset from the list to edit its model and generation settings.",
"presetName": "Preset name", "presetName": "Preset name",
"presetNameHelp": "A short name used throughout model settings.", "presetNameHelp": "A short name used throughout model settings.",
"presetNamePlaceholder": "Fast writing", "presetNamePlaceholder": "Fast writing",
-3
View File
@@ -408,9 +408,6 @@
"newPresetHelp": "Guarda un modelo reutilizable y sus ajustes de generación.", "newPresetHelp": "Guarda un modelo reutilizable y sus ajustes de generación.",
"presets": "Preajustes de modelo", "presets": "Preajustes de modelo",
"editPreset": "Editar preajuste", "editPreset": "Editar preajuste",
"backToPresets": "Volver a los preajustes de modelo",
"selectPreset": "Seleccionar un preajuste",
"selectPresetHelp": "Selecciona un preajuste de la lista para editar su modelo y los ajustes de generación.",
"presetName": "Nombre del preajuste", "presetName": "Nombre del preajuste",
"presetNameHelp": "Un nombre corto para usar en los ajustes de modelos.", "presetNameHelp": "Un nombre corto para usar en los ajustes de modelos.",
"presetNamePlaceholder": "Escritura rápida", "presetNamePlaceholder": "Escritura rápida",
-3
View File
@@ -408,9 +408,6 @@
"newPresetHelp": "Enregistrez un modèle réutilisable et ses paramètres de génération.", "newPresetHelp": "Enregistrez un modèle réutilisable et ses paramètres de génération.",
"presets": "Préréglages de modèle", "presets": "Préréglages de modèle",
"editPreset": "Modifier le préréglage", "editPreset": "Modifier le préréglage",
"backToPresets": "Retour aux préréglages de modèle",
"selectPreset": "Sélectionner un préréglage",
"selectPresetHelp": "Sélectionnez un préréglage dans la liste pour modifier son modèle et ses paramètres de génération.",
"presetName": "Nom du préréglage", "presetName": "Nom du préréglage",
"presetNameHelp": "Un nom court utilisé dans les paramètres des modèles.", "presetNameHelp": "Un nom court utilisé dans les paramètres des modèles.",
"presetNamePlaceholder": "Rédaction rapide", "presetNamePlaceholder": "Rédaction rapide",
-3
View File
@@ -408,9 +408,6 @@
"newPresetHelp": "Simpan model yang dapat digunakan kembali beserta pengaturan generasinya.", "newPresetHelp": "Simpan model yang dapat digunakan kembali beserta pengaturan generasinya.",
"presets": "Prasetel model", "presets": "Prasetel model",
"editPreset": "Ubah prasetel", "editPreset": "Ubah prasetel",
"backToPresets": "Kembali ke prasetel model",
"selectPreset": "Pilih prasetel",
"selectPresetHelp": "Pilih prasetel dari daftar untuk mengubah model dan pengaturan generasinya.",
"presetName": "Nama prasetel", "presetName": "Nama prasetel",
"presetNameHelp": "Nama singkat yang digunakan di pengaturan model.", "presetNameHelp": "Nama singkat yang digunakan di pengaturan model.",
"presetNamePlaceholder": "Menulis cepat", "presetNamePlaceholder": "Menulis cepat",
-3
View File
@@ -408,9 +408,6 @@
"newPresetHelp": "再利用できるモデルと生成設定を保存します。", "newPresetHelp": "再利用できるモデルと生成設定を保存します。",
"presets": "モデルプリセット", "presets": "モデルプリセット",
"editPreset": "プリセットを編集", "editPreset": "プリセットを編集",
"backToPresets": "モデルプリセットに戻る",
"selectPreset": "プリセットを選択",
"selectPresetHelp": "リストからプリセットを選択して、モデルと生成設定を編集します。",
"presetName": "プリセット名", "presetName": "プリセット名",
"presetNameHelp": "モデル設定で使用する短い名前です。", "presetNameHelp": "モデル設定で使用する短い名前です。",
"presetNamePlaceholder": "高速執筆", "presetNamePlaceholder": "高速執筆",
-3
View File
@@ -408,9 +408,6 @@
"newPresetHelp": "재사용할 모델과 생성 설정을 저장합니다.", "newPresetHelp": "재사용할 모델과 생성 설정을 저장합니다.",
"presets": "모델 프리셋", "presets": "모델 프리셋",
"editPreset": "프리셋 편집", "editPreset": "프리셋 편집",
"backToPresets": "모델 프리셋으로 돌아가기",
"selectPreset": "프리셋 선택",
"selectPresetHelp": "목록에서 프리셋을 선택하여 모델과 생성 설정을 편집하세요.",
"presetName": "프리셋 이름", "presetName": "프리셋 이름",
"presetNameHelp": "모델 설정에서 사용할 짧은 이름입니다.", "presetNameHelp": "모델 설정에서 사용할 짧은 이름입니다.",
"presetNamePlaceholder": "빠른 작성", "presetNamePlaceholder": "빠른 작성",
-3
View File
@@ -132,9 +132,6 @@
"newPresetHelp": "Salve um modelo reutilizável e suas configurações de geração.", "newPresetHelp": "Salve um modelo reutilizável e suas configurações de geração.",
"presets": "Predefinições de modelo", "presets": "Predefinições de modelo",
"editPreset": "Editar predefinição", "editPreset": "Editar predefinição",
"backToPresets": "Voltar às predefinições de modelo",
"selectPreset": "Selecionar uma predefinição",
"selectPresetHelp": "Selecione uma predefinição na lista para editar o modelo e as configurações de geração.",
"presetName": "Nome da predefinição", "presetName": "Nome da predefinição",
"presetNameHelp": "Um nome curto usado nas configurações de modelo.", "presetNameHelp": "Um nome curto usado nas configurações de modelo.",
"presetNamePlaceholder": "Escrita rápida", "presetNamePlaceholder": "Escrita rápida",
-3
View File
@@ -408,9 +408,6 @@
"newPresetHelp": "Lưu mô hình có thể tái sử dụng cùng các thiết lập sinh nội dung.", "newPresetHelp": "Lưu mô hình có thể tái sử dụng cùng các thiết lập sinh nội dung.",
"presets": "Cấu hình mô hình đặt trước", "presets": "Cấu hình mô hình đặt trước",
"editPreset": "Sửa cấu hình đặt trước", "editPreset": "Sửa cấu hình đặt trước",
"backToPresets": "Quay lại cấu hình mô hình đặt trước",
"selectPreset": "Chọn một cấu hình đặt trước",
"selectPresetHelp": "Chọn một cấu hình đặt trước trong danh sách để chỉnh sửa mô hình và cài đặt sinh nội dung.",
"presetName": "Tên cấu hình đặt trước", "presetName": "Tên cấu hình đặt trước",
"presetNameHelp": "Tên ngắn dùng trong phần thiết lập mô hình.", "presetNameHelp": "Tên ngắn dùng trong phần thiết lập mô hình.",
"presetNamePlaceholder": "Viết nhanh", "presetNamePlaceholder": "Viết nhanh",
-3
View File
@@ -132,9 +132,6 @@
"newPresetHelp": "保存可复用的模型及其生成参数。", "newPresetHelp": "保存可复用的模型及其生成参数。",
"presets": "模型预设", "presets": "模型预设",
"editPreset": "编辑预设", "editPreset": "编辑预设",
"backToPresets": "返回模型预设",
"selectPreset": "选择一个预设",
"selectPresetHelp": "从列表中选择一个预设,编辑它的模型和生成参数。",
"presetName": "预设名称", "presetName": "预设名称",
"presetNameHelp": "在模型设置中使用的简短名称。", "presetNameHelp": "在模型设置中使用的简短名称。",
"presetNamePlaceholder": "快速写作", "presetNamePlaceholder": "快速写作",
-3
View File
@@ -408,9 +408,6 @@
"newPresetHelp": "儲存可重複使用的模型與生成參數。", "newPresetHelp": "儲存可重複使用的模型與生成參數。",
"presets": "模型預設", "presets": "模型預設",
"editPreset": "編輯預設", "editPreset": "編輯預設",
"backToPresets": "返回模型預設",
"selectPreset": "選擇一個預設",
"selectPresetHelp": "從列表中選擇一個預設,編輯它的模型與生成參數。",
"presetName": "預設名稱", "presetName": "預設名稱",
"presetNameHelp": "模型設定中使用的簡短名稱。", "presetNameHelp": "模型設定中使用的簡短名稱。",
"presetNamePlaceholder": "快速寫作", "presetNamePlaceholder": "快速寫作",
+12 -15
View File
@@ -2137,7 +2137,7 @@ describe("SettingsView Apps catalog", () => {
expect(reasoningEffort).toHaveValue("provider-native-mode"); expect(reasoningEffort).toHaveValue("provider-native-mode");
}); });
it("associates preset selection with the responsive detail pane", async () => { it("expands the model preset editor directly below the selected row", async () => {
vi.stubGlobal( vi.stubGlobal(
"fetch", "fetch",
vi.fn(async (input: RequestInfo | URL) => { vi.fn(async (input: RequestInfo | URL) => {
@@ -2157,22 +2157,23 @@ describe("SettingsView Apps catalog", () => {
const row = await screen.findByTestId("model-call-order-row-primary"); const row = await screen.findByTestId("model-call-order-row-primary");
const trigger = within(row).getAllByRole("button")[0]; const trigger = within(row).getAllByRole("button")[0];
const detail = document.getElementById("model-preset-detail"); expect(screen.queryByTestId("model-preset-editor")).not.toBeInTheDocument();
expect(detail).toHaveClass("hidden", "xl:block"); expect(trigger).toHaveAttribute("aria-expanded", "false");
expect(trigger).toHaveAttribute("aria-controls", "model-preset-detail");
fireEvent.click(trigger); fireEvent.click(trigger);
const editor = screen.getByTestId("model-preset-editor");
expect(trigger).toHaveAttribute("aria-pressed", "true"); expect(trigger).toHaveAttribute("aria-pressed", "true");
expect(detail).not.toHaveClass("hidden"); expect(trigger).toHaveAttribute("aria-expanded", "true");
expect(within(detail as HTMLElement).getByDisplayValue("Primary")).toBeInTheDocument(); expect(trigger).toHaveAttribute("aria-controls", "model-preset-editor");
expect(row.nextElementSibling).toBe(editor);
expect(editor).toHaveClass("slide-in-from-top-1");
expect(within(editor).getByDisplayValue("Primary")).toBeInTheDocument();
const backButton = screen.getByRole("button", { name: "Back to model presets" }); fireEvent.click(trigger);
expect(backButton).toHaveClass("xl:hidden");
fireEvent.click(backButton);
expect(trigger).toHaveAttribute("aria-pressed", "false"); expect(trigger).toHaveAttribute("aria-expanded", "false");
expect(detail).toHaveClass("hidden", "xl:block"); expect(screen.queryByTestId("model-preset-editor")).not.toBeInTheDocument();
}); });
it("drags model presets to reorder and saves the model call order immediately", async () => { it("drags model presets to reorder and saves the model call order immediately", async () => {
@@ -4048,10 +4049,6 @@ describe("SettingsView Apps catalog", () => {
expect(screen.queryByRole("dialog", { name: "New model preset" })).not.toBeInTheDocument(); expect(screen.queryByRole("dialog", { name: "New model preset" })).not.toBeInTheDocument();
expect(screen.getByPlaceholderText("Fast writing")).toHaveValue(""); expect(screen.getByPlaceholderText("Fast writing")).toHaveValue("");
expect(screen.queryByText("Temperature")).not.toBeInTheDocument(); expect(screen.queryByText("Temperature")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Back to model presets" }));
expect(screen.queryByPlaceholderText("Fast writing")).not.toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "New model preset" }));
fireEvent.click(screen.getByRole("button", { name: /Advanced options/ })); fireEvent.click(screen.getByRole("button", { name: /Advanced options/ }));
expect(screen.getByText("Temperature")).toBeInTheDocument(); expect(screen.getByText("Temperature")).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Cancel" })); fireEvent.click(screen.getByRole("button", { name: "Cancel" }));