From e875f291859a143d85dd097975d5f101564a56b8 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:52:15 +0800 Subject: [PATCH] fix(image): allow custom model ids --- .../src/components/settings/SettingsView.tsx | 171 +++++++++++++++--- webui/src/tests/settings-view.test.tsx | 16 +- 2 files changed, 165 insertions(+), 22 deletions(-) diff --git a/webui/src/components/settings/SettingsView.tsx b/webui/src/components/settings/SettingsView.tsx index ebfdc527..34c15fd9 100644 --- a/webui/src/components/settings/SettingsView.tsx +++ b/webui/src/components/settings/SettingsView.tsx @@ -3574,11 +3574,10 @@ function ImageGenerationSettings({ IMAGE_SIZE_OPTIONS.map((value) => ({ name: value, label: value })), form.defaultImageSize, ); - const modelOptions = optionRowsWithCurrent( - (selectedProvider?.models ?? []).map((model) => ({ name: model, label: model })), - form.model, - ); - const hasModelCatalog = Boolean(selectedProvider?.models?.length); + const modelOptions = (selectedProvider?.models ?? []).map((model) => ({ + name: model, + label: model, + })); const selectProvider = (provider: string) => { const nextProvider = settings.image_generation.providers.find((row) => row.name === provider); @@ -3649,22 +3648,21 @@ function ImageGenerationSettings({ title={tx("settings.rows.imageModel", "Image model")} description={tx("settings.help.imageModel", "Model name sent to the selected image provider.")} > - {hasModelCatalog ? ( - onChangeForm((prev) => ({ ...prev, model }))} - triggerClassName="w-[min(300px,70vw)]" - contentClassName="w-[320px]" - /> - ) : ( - onChangeForm((prev) => ({ ...prev, model: event.target.value }))} - className="h-8 w-[min(300px,70vw)] rounded-full text-[13px]" - /> - )} + onChangeForm((prev) => ({ ...prev, model }))} + /> ; + value: string; + emptyLabel: string; + searchPlaceholder: string; + emptyMessage: string; + useCustomLabel: string; + onChange: (value: string) => void; +}) { + const [open, setOpen] = useState(false); + const [query, setQuery] = useState(""); + const normalizedQuery = query.trim().toLowerCase(); + const customCandidate = query.trim(); + const exactMatch = options.some((option) => option.name === customCandidate); + const visibleOptions = options.filter((option) => + [option.name, option.label].some((field) => field.toLowerCase().includes(normalizedQuery)), + ); + + const selectValue = (nextValue: string) => { + onChange(nextValue); + setQuery(""); + setOpen(false); + }; + + return ( + { + setOpen(nextOpen); + if (!nextOpen) setQuery(""); + }} + > + + + + +
+
+ + setQuery(event.target.value)} + onKeyDown={(event) => { + event.stopPropagation(); + if (event.key === "Enter" && customCandidate) { + event.preventDefault(); + selectValue(customCandidate); + } + }} + placeholder={searchPlaceholder} + aria-label={searchPlaceholder} + className="h-8 rounded-full pl-8 pr-3 text-[12px]" + /> +
+
+ + {visibleOptions.length ? ( +
+ {visibleOptions.map((option) => { + const selected = option.name === value; + return ( + selectValue(option.name)} + className={cn( + "flex cursor-default items-center justify-between gap-2 rounded-[12px] px-2.5 py-2 text-[13px]", + "focus:bg-muted/85 focus:text-foreground", + selected && "bg-muted/80 text-foreground focus:bg-muted", + )} + > + {option.label} + {selected ? : null} + + ); + })} +
+ ) : !customCandidate ? ( +
+ {emptyMessage} +
+ ) : null} + + {customCandidate && !exactMatch ? ( + <> + {visibleOptions.length ? : null} + selectValue(customCandidate)} + className="flex cursor-default items-center gap-2 rounded-[12px] px-2 py-1.5 text-[12px] focus:bg-muted/85" + > + + + + + {useCustomLabel}{" "} + “{customCandidate}” + + + + ) : null} +
+
+ ); +} + function ModelIdPicker({ token, settings, diff --git a/webui/src/tests/settings-view.test.tsx b/webui/src/tests/settings-view.test.tsx index 70b59eb9..dd614ed2 100644 --- a/webui/src/tests/settings-view.test.tsx +++ b/webui/src/tests/settings-view.test.tsx @@ -2323,9 +2323,23 @@ describe("SettingsView Apps catalog", () => { expect(screen.getByRole("button", { name: "imagen-4.0-generate-001" })).toBeInTheDocument(), ); + fireEvent.pointerDown(screen.getByRole("button", { name: "imagen-4.0-generate-001" })); + const modelInput = await screen.findByRole("textbox", { name: "Search or type model ID" }); + fireEvent.change(modelInput, { target: { value: "imagen-5-preview" } }); + fireEvent.click(await screen.findByRole("menuitem", { name: "Use “imagen-5-preview”" })); + expect(await screen.findByRole("button", { name: "imagen-5-preview" })).toBeInTheDocument(); + fireEvent.pointerDown(screen.getByRole("button", { name: "Gemini" })); fireEvent.click(await screen.findByRole("menuitem", { name: "Custom" })); - expect(screen.getByDisplayValue("imagen-4.0-generate-001")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "imagen-5-preview" })).toBeInTheDocument(); + + fireEvent.pointerDown(screen.getByRole("button", { name: "imagen-5-preview" })); + const customProviderInput = await screen.findByRole("textbox", { + name: "Search or type model ID", + }); + fireEvent.change(customProviderInput, { target: { value: "private/image-v2" } }); + fireEvent.keyDown(customProviderInput, { key: "Enter" }); + expect(await screen.findByRole("button", { name: "private/image-v2" })).toBeInTheDocument(); }); it("keeps the default model distinct from the active named configuration", async () => {