From ada07aa799a6855872164f0cbd536f98e7d246f9 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Fri, 7 Aug 2026 07:55:29 +0800 Subject: [PATCH] feat(webui): add responsive model preset detail pane --- .../src/components/settings/SettingsView.tsx | 109 +++++++++++++----- webui/src/i18n/locales/en/common.json | 3 + webui/src/i18n/locales/es/common.json | 3 + webui/src/i18n/locales/fr/common.json | 3 + webui/src/i18n/locales/id/common.json | 3 + webui/src/i18n/locales/ja/common.json | 3 + webui/src/i18n/locales/ko/common.json | 3 + webui/src/i18n/locales/pt-BR/common.json | 3 + webui/src/i18n/locales/vi/common.json | 3 + webui/src/i18n/locales/zh-CN/common.json | 3 + webui/src/i18n/locales/zh-TW/common.json | 3 + webui/src/tests/settings-view.test.tsx | 42 +++++++ 12 files changed, 155 insertions(+), 26 deletions(-) diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index 194163a31..c95cc9b2d 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -3375,10 +3375,9 @@ function ModelsSettings({ ); const callOrderBusy = orderSaving || saving; const selectPreset = (preset: SettingsPayload["model_presets"][number]) => { - const toggleCurrentPreset = !creating && selectedPreset?.name === preset.name; onSelectConfiguration(); - if (toggleCurrentPreset) { - setEditorOpen((open) => !open); + if (!creating && selectedPreset?.name === preset.name) { + setEditorOpen(true); return; } setForm((prev) => ({ @@ -3394,6 +3393,11 @@ function ModelsSettings({ })); setEditorOpen(true); }; + const closeEditor = () => { + if (creatingSaving) return; + if (creating) onCancelCreate(); + setEditorOpen(false); + }; const moveCallOrderItem = (index: number, offset: -1 | 1) => { if (callOrderBusy) return; @@ -3473,7 +3477,13 @@ function ModelsSettings({ ) : ( - <> +
+
{presetRows.map(({ key, name, orderIndex, preset }) => { const ordered = orderIndex >= 0; @@ -3495,6 +3505,8 @@ function ModelsSettings({ isDropTarget && draggedCallOrderIndex !== null && draggedCallOrderIndex < orderIndex; + const isSelected = + editorOpen && !creating && selectedPreset?.name === name; return (
@@ -3580,8 +3593,8 @@ function ModelsSettings({ )}
- - )} - - {editorOpen && (selectedPreset || creating) ? ( - <> -
- - {creating - ? tx("settings.models.newPreset", "New model preset") - : tx("settings.models.editPreset", "Edit preset")} - -
+
+
+ {editorOpen && (selectedPreset || creating) ? ( + <> +
+ + + + {creating + ? tx("settings.models.newPreset", "New model preset") + : selectedPreset?.label} + + + {creating + ? tx( + "settings.models.newPresetHelp", + "Save a reusable model and its generation settings.", + ) + : tx("settings.models.editPreset", "Edit preset")} + + +
{ - setEditorOpen(false); - onCancelCreate(); - }} + onClick={closeEditor} > {tx("settings.actions.cancel", "Cancel")} @@ -3860,8 +3897,28 @@ function ModelsSettings({
- - ) : null} + + ) : ( +
+
+ + + +

+ {tx("settings.models.selectPreset", "Select a preset")} +

+

+ {tx( + "settings.models.selectPresetHelp", + "Select a preset from the list to edit its model and generation settings.", + )} +

+
+
+ )} +
+ + )} diff --git a/webui/src/i18n/locales/en/common.json b/webui/src/i18n/locales/en/common.json index 9d47eb82b..817117543 100644 --- a/webui/src/i18n/locales/en/common.json +++ b/webui/src/i18n/locales/en/common.json @@ -132,6 +132,9 @@ "newPresetHelp": "Save a reusable model and its generation settings.", "presets": "Model presets", "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", "presetNameHelp": "A short name used throughout model settings.", "presetNamePlaceholder": "Fast writing", diff --git a/webui/src/i18n/locales/es/common.json b/webui/src/i18n/locales/es/common.json index b0a15b096..bed369d16 100644 --- a/webui/src/i18n/locales/es/common.json +++ b/webui/src/i18n/locales/es/common.json @@ -408,6 +408,9 @@ "newPresetHelp": "Guarda un modelo reutilizable y sus ajustes de generación.", "presets": "Preajustes de modelo", "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", "presetNameHelp": "Un nombre corto para usar en los ajustes de modelos.", "presetNamePlaceholder": "Escritura rápida", diff --git a/webui/src/i18n/locales/fr/common.json b/webui/src/i18n/locales/fr/common.json index 45efeb992..c470e4561 100644 --- a/webui/src/i18n/locales/fr/common.json +++ b/webui/src/i18n/locales/fr/common.json @@ -408,6 +408,9 @@ "newPresetHelp": "Enregistrez un modèle réutilisable et ses paramètres de génération.", "presets": "Préréglages de modèle", "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", "presetNameHelp": "Un nom court utilisé dans les paramètres des modèles.", "presetNamePlaceholder": "Rédaction rapide", diff --git a/webui/src/i18n/locales/id/common.json b/webui/src/i18n/locales/id/common.json index b420bae1f..09d20cb2d 100644 --- a/webui/src/i18n/locales/id/common.json +++ b/webui/src/i18n/locales/id/common.json @@ -408,6 +408,9 @@ "newPresetHelp": "Simpan model yang dapat digunakan kembali beserta pengaturan generasinya.", "presets": "Prasetel model", "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", "presetNameHelp": "Nama singkat yang digunakan di pengaturan model.", "presetNamePlaceholder": "Menulis cepat", diff --git a/webui/src/i18n/locales/ja/common.json b/webui/src/i18n/locales/ja/common.json index 332a16e0b..6e13d2261 100644 --- a/webui/src/i18n/locales/ja/common.json +++ b/webui/src/i18n/locales/ja/common.json @@ -408,6 +408,9 @@ "newPresetHelp": "再利用できるモデルと生成設定を保存します。", "presets": "モデルプリセット", "editPreset": "プリセットを編集", + "backToPresets": "モデルプリセットに戻る", + "selectPreset": "プリセットを選択", + "selectPresetHelp": "リストからプリセットを選択して、モデルと生成設定を編集します。", "presetName": "プリセット名", "presetNameHelp": "モデル設定で使用する短い名前です。", "presetNamePlaceholder": "高速執筆", diff --git a/webui/src/i18n/locales/ko/common.json b/webui/src/i18n/locales/ko/common.json index 361449be6..068810d7e 100644 --- a/webui/src/i18n/locales/ko/common.json +++ b/webui/src/i18n/locales/ko/common.json @@ -408,6 +408,9 @@ "newPresetHelp": "재사용할 모델과 생성 설정을 저장합니다.", "presets": "모델 프리셋", "editPreset": "프리셋 편집", + "backToPresets": "모델 프리셋으로 돌아가기", + "selectPreset": "프리셋 선택", + "selectPresetHelp": "목록에서 프리셋을 선택하여 모델과 생성 설정을 편집하세요.", "presetName": "프리셋 이름", "presetNameHelp": "모델 설정에서 사용할 짧은 이름입니다.", "presetNamePlaceholder": "빠른 작성", diff --git a/webui/src/i18n/locales/pt-BR/common.json b/webui/src/i18n/locales/pt-BR/common.json index 7f499f650..05c8495dd 100644 --- a/webui/src/i18n/locales/pt-BR/common.json +++ b/webui/src/i18n/locales/pt-BR/common.json @@ -132,6 +132,9 @@ "newPresetHelp": "Salve um modelo reutilizável e suas configurações de geração.", "presets": "Predefinições de modelo", "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", "presetNameHelp": "Um nome curto usado nas configurações de modelo.", "presetNamePlaceholder": "Escrita rápida", diff --git a/webui/src/i18n/locales/vi/common.json b/webui/src/i18n/locales/vi/common.json index 7586d6aab..9cada0a46 100644 --- a/webui/src/i18n/locales/vi/common.json +++ b/webui/src/i18n/locales/vi/common.json @@ -408,6 +408,9 @@ "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", "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", "presetNameHelp": "Tên ngắn dùng trong phần thiết lập mô hình.", "presetNamePlaceholder": "Viết nhanh", diff --git a/webui/src/i18n/locales/zh-CN/common.json b/webui/src/i18n/locales/zh-CN/common.json index 4290de08a..9e6200a4e 100644 --- a/webui/src/i18n/locales/zh-CN/common.json +++ b/webui/src/i18n/locales/zh-CN/common.json @@ -132,6 +132,9 @@ "newPresetHelp": "保存可复用的模型及其生成参数。", "presets": "模型预设", "editPreset": "编辑预设", + "backToPresets": "返回模型预设", + "selectPreset": "选择一个预设", + "selectPresetHelp": "从列表中选择一个预设,编辑它的模型和生成参数。", "presetName": "预设名称", "presetNameHelp": "在模型设置中使用的简短名称。", "presetNamePlaceholder": "快速写作", diff --git a/webui/src/i18n/locales/zh-TW/common.json b/webui/src/i18n/locales/zh-TW/common.json index 593c79c9f..6a7e163eb 100644 --- a/webui/src/i18n/locales/zh-TW/common.json +++ b/webui/src/i18n/locales/zh-TW/common.json @@ -408,6 +408,9 @@ "newPresetHelp": "儲存可重複使用的模型與生成參數。", "presets": "模型預設", "editPreset": "編輯預設", + "backToPresets": "返回模型預設", + "selectPreset": "選擇一個預設", + "selectPresetHelp": "從列表中選擇一個預設,編輯它的模型與生成參數。", "presetName": "預設名稱", "presetNameHelp": "模型設定中使用的簡短名稱。", "presetNamePlaceholder": "快速寫作", diff --git a/webui/src/tests/settings-view.test.tsx b/webui/src/tests/settings-view.test.tsx index 954ff7fa8..67908d160 100644 --- a/webui/src/tests/settings-view.test.tsx +++ b/webui/src/tests/settings-view.test.tsx @@ -2137,6 +2137,44 @@ describe("SettingsView Apps catalog", () => { expect(reasoningEffort).toHaveValue("provider-native-mode"); }); + it("associates preset selection with the responsive detail pane", async () => { + vi.stubGlobal( + "fetch", + vi.fn(async (input: RequestInfo | URL) => { + const url = String(input); + if (url === "/api/settings") return jsonResponse(settingsPayload()); + if (url === "/api/settings/cli-apps") { + return jsonResponse({ apps: [], installed_count: 0 }); + } + if (url === "/api/settings/mcp-presets") { + return jsonResponse({ presets: [], installed_count: 0 }); + } + return { ok: false, status: 404, json: async () => ({}) } as Response; + }), + ); + + renderSettingsView({ initialSection: "models" }); + + const row = await screen.findByTestId("model-call-order-row-primary"); + const trigger = within(row).getAllByRole("button")[0]; + const detail = document.getElementById("model-preset-detail"); + expect(detail).toHaveClass("hidden", "xl:block"); + expect(trigger).toHaveAttribute("aria-controls", "model-preset-detail"); + + fireEvent.click(trigger); + + expect(trigger).toHaveAttribute("aria-pressed", "true"); + expect(detail).not.toHaveClass("hidden"); + expect(within(detail as HTMLElement).getByDisplayValue("Primary")).toBeInTheDocument(); + + const backButton = screen.getByRole("button", { name: "Back to model presets" }); + expect(backButton).toHaveClass("xl:hidden"); + fireEvent.click(backButton); + + expect(trigger).toHaveAttribute("aria-pressed", "false"); + expect(detail).toHaveClass("hidden", "xl:block"); + }); + it("drags model presets to reorder and saves the model call order immediately", async () => { const { payload, backupPreset } = settingsPayloadWithBackup(); const updatedPayload: SettingsPayload = { @@ -4010,6 +4048,10 @@ describe("SettingsView Apps catalog", () => { expect(screen.queryByRole("dialog", { name: "New model preset" })).not.toBeInTheDocument(); expect(screen.getByPlaceholderText("Fast writing")).toHaveValue(""); 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/ })); expect(screen.getByText("Temperature")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Cancel" }));