diff --git a/modules/control/run.py b/modules/control/run.py index 02c186d55..83b6bb483 100644 --- a/modules/control/run.py +++ b/modules/control/run.py @@ -444,7 +444,7 @@ def control_run(state: str = '', # pylint: disable=keyword-arg-before-vararg if sampler_index is None: log.warning('Sampler: invalid') sampler_index = 0 - if hr_sampler_index is None: + if hr_sampler_index is None or hr_sampler_index == 'Same as primary': hr_sampler_index = sampler_index if isinstance(extra, list): extra = create_override_settings_dict(extra) diff --git a/modules/detailer/detailer.py b/modules/detailer/detailer.py index 4556e242b..10cb1c999 100644 --- a/modules/detailer/detailer.py +++ b/modules/detailer/detailer.py @@ -537,12 +537,12 @@ class Detailer(): renoise_end = gr.Slider(minimum=0.0, maximum=1.0, step=0.01, label='Renoise end', value=shared.opts.detailer_sigma_adjust_max, elem_id=f"{tab}_detailer_renoise_end") sampler_block = None if tab == 'extras': # fold the standalone sampler settings into the detailer accordion; values applied per-job in make_processing, never global opts - from modules import sd_samplers - sd_samplers.set_samplers() - sampler_choices = [s.name for s in sd_samplers.visible_samplers() if s.name != 'Same as primary'] + from modules import ui_sections + sampler_choices, default_value, filtered = ui_sections.sampler_choices() with gr.Accordion('Sampler', open=False, elem_id=f"{tab}_detailer_sampler_accordion", elem_classes=["small-accordion"]): with gr.Row(): - d_sampler = gr.Dropdown(label='Sampling method', choices=sampler_choices, value='Default', elem_id=f"{tab}_detailer_sampler") + ui_sections.create_filter_indicator(tab, 'Sampler', filtered) + d_sampler = gr.Dropdown(label='Sampling method', choices=sampler_choices, value=default_value, type='value', elem_id=f"{tab}_detailer_sampler") d_prediction = gr.Dropdown(label='Prediction method', choices=['default', 'epsilon', 'sample', 'v_prediction', 'flow_prediction'], value='default', elem_id=f"{tab}_detailer_prediction") with gr.Row(): d_shift = gr.Slider(label='Flow shift', minimum=0, maximum=10, step=0.1, value=shared.opts.schedulers_shift, elem_id=f"{tab}_detailer_shift") diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index 632f3f1aa..f8fd1ab84 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -296,6 +296,23 @@ def connect_paste(button, local_paste_fields, input_comp, override_settings_comp res.append(v) applied[key] = v else: + if key in ('Sampler', 'Hires sampler') and isinstance(v, str): + from modules import ui_sections + choices, value, _ = ui_sections.sampler_choices(selected=v, same_as_primary=key == 'Hires sampler') + res.append(gr.update(choices=choices, value=value)) + applied[key] = v + continue + if getattr(output, 'elem_id', '').endswith('_resize_name') and isinstance(v, str): + from modules import modelloader, shared, ui_sections + modelloader.load_upscalers() + choices = [upscaler.name for upscaler in shared.sd_upscalers] + if output.elem_id.startswith(('control_after', 'control_mask')): + choices = [choice for choice in choices if not choice.lower().startswith('latent')] + if v in choices: + choices, _ = ui_sections.upscaler_choices(choices, selected=v) + res.append(gr.update(choices=choices, value=v)) + applied[key] = v + continue if isinstance(v, str) and v.strip() == '' and key in {'Prompt', 'Negative prompt'}: debug(f'Paste skip empty: "{key}"') res.append(gr.update()) diff --git a/modules/img2img.py b/modules/img2img.py index 6c178da85..d01992811 100644 --- a/modules/img2img.py +++ b/modules/img2img.py @@ -193,6 +193,8 @@ def img2img(id_task: str, state: str, mode: int, if sampler_index is None: log.warning('Sampler: invalid') sampler_index = 0 + if hr_sampler_index is None or hr_sampler_index == 'Same as primary': + hr_sampler_index = sampler_index mode = int(mode) image = None diff --git a/modules/processing_helpers.py b/modules/processing_helpers.py index dfaabc65d..5e99d1f2b 100644 --- a/modules/processing_helpers.py +++ b/modules/processing_helpers.py @@ -159,9 +159,11 @@ def images_tensor_to_samples(image, approximation=None, model=None): # pylint: d return x_latent -def get_sampler_name(sampler_index: int | None = None, img: bool = False) -> str: +def get_sampler_name(sampler_index: int | str | None = None, img: bool = False) -> str: sampler_index = sampler_index or 0 - if len(sd_samplers.samplers) > sampler_index: + if isinstance(sampler_index, str) and any(sampler.name == sampler_index for sampler in sd_samplers.samplers): + sampler_name = sampler_index + elif isinstance(sampler_index, int) and 0 <= sampler_index < len(sd_samplers.samplers): sampler_name = sd_samplers.samplers[sampler_index].name else: sampler_name = "Default" diff --git a/modules/txt2img.py b/modules/txt2img.py index af0b814bc..4c333f0fa 100644 --- a/modules/txt2img.py +++ b/modules/txt2img.py @@ -41,7 +41,7 @@ def txt2img(id_task, state, if sampler_index is None: log.warning('Sampler: invalid') sampler_index = 0 - if hr_sampler_index is None: + if hr_sampler_index is None or hr_sampler_index == 'Same as primary': hr_sampler_index = sampler_index p = processing.StableDiffusionProcessingTxt2Img( diff --git a/modules/ui_choices.py b/modules/ui_choices.py new file mode 100644 index 000000000..f5c59a986 --- /dev/null +++ b/modules/ui_choices.py @@ -0,0 +1,17 @@ +"""Helpers for filtering display-only UI choice lists.""" + + +def filter_ui_choices(choices: list[str], preferences: list[str] | None = None, selected: str | None = None) -> tuple[list[str], bool]: + """Return choices selected for display without changing the underlying catalog. + + Empty or stale preferences leave the complete list visible. A current value is + always retained so loading a saved workflow cannot silently replace it. + """ + available = list(dict.fromkeys(choices)) + preferred = set(preferences or []) + filtered = [choice for choice in available if choice in preferred] + if not filtered: + return available, False + if selected in available and selected not in filtered: + filtered.append(selected) + return filtered, len(filtered) < len(available) diff --git a/modules/ui_definitions.py b/modules/ui_definitions.py index 825722aa3..865e55f8e 100644 --- a/modules/ui_definitions.py +++ b/modules/ui_definitions.py @@ -51,6 +51,11 @@ def list_samplers(): return modules.sd_samplers.all_samplers +def list_upscalers(): + from modules import shared # pylint: disable=redefined-outer-name + return [upscaler.name for upscaler in shared.sd_upscalers] + + def get_openvino_device_list(): try: import modules.intel.openvino # pylint: disable=redefined-outer-name @@ -769,6 +774,12 @@ def create_settings(cmd_opts): "disable_all_extensions": OptionInfo("none", "Disable all extensions", gr.Radio, {"choices": ["none", "user", "all"]}), })) + # --- Sampler Settings --- + options_templates.update(options_section(('sampler', "Sampler Settings"), { + "show_samplers": OptionInfo([], "Show samplers in user interface", gr.CheckboxGroup, lambda: {"choices": [x.name for x in list_samplers()]}, refresh=list_samplers), + "show_upscalers": OptionInfo([], "Show upscalers in user interface", gr.CheckboxGroup, lambda: {"choices": list_upscalers()}, refresh=refresh_upscalers), + })) + # --- Hidden Options --- options_templates.update( options_section( @@ -830,7 +841,6 @@ def create_settings(cmd_opts): "control_move_processor": OptionInfo(False, "Processor move to CPU when complete", gr.Checkbox, {"visible": False}), "control_unload_processor": OptionInfo(False, "Processor unload after use", gr.Checkbox, {"visible": False}), # sampler settings are handled separately - "show_samplers": OptionInfo([], "Show samplers in user interface", gr.CheckboxGroup, lambda: {"choices": [x.name for x in list_samplers()], "visible": False}), "eta_noise_seed_delta": OptionInfo(0, "Noise seed delta (eta)", gr.Number, {"precision": 0, "visible": False}), "scheduler_eta": OptionInfo(1.0, "Noise multiplier (eta)", gr.Slider, {"minimum": 0.0, "maximum": 1.0, "step": 0.01, "visible": False}), "schedulers_solver_order": OptionInfo(0, "Solver order (where", gr.Slider, {"minimum": 0, "maximum": 5, "step": 1, "visible": False}), diff --git a/modules/ui_sections.py b/modules/ui_sections.py index a57a99074..4212ffce7 100644 --- a/modules/ui_sections.py +++ b/modules/ui_sections.py @@ -2,6 +2,7 @@ import gradio as gr from modules import shared, modelloader, ui_symbols, ui_common, sd_samplers from modules.logger import log from modules.ui_components import ToolButton +from modules.ui_choices import filter_ui_choices from modules.caption import caption @@ -213,13 +214,41 @@ def create_color_inputs(tab): return grading_brightness, grading_contrast, grading_saturation, grading_hue, grading_gamma, grading_sharpness, grading_color_temp, grading_shadows, grading_midtones, grading_highlights, grading_clahe_clip, grading_clahe_grid, grading_shadows_tint, grading_highlights_tint, grading_split_tone_balance, grading_vignette, grading_grain, grading_lut_cube_file, grading_lut_strength +def sampler_choices(choices=None, selected='Default', same_as_primary=False): + """Build display-only sampler choices without changing the sampler catalog.""" + if choices is None: + sd_samplers.set_samplers() + choices = [sampler for sampler in sd_samplers.samplers if sampler.name != 'Same as primary'] + names = [choice.name if hasattr(choice, 'name') else choice for choice in choices] + visible, filtered = filter_ui_choices(names, shared.opts.show_samplers, selected) + if same_as_primary: + visible.insert(0, 'Same as primary') + value = selected if selected in visible else visible[0] + return visible, value, filtered + + +def upscaler_choices(choices, selected=None): + """Build display-only upscaler choices without changing available upscalers.""" + return filter_ui_choices(choices, shared.opts.show_upscalers, selected) + + +def create_filter_indicator(tabname, kind, filtered): + if not filtered: + return None + indicator = gr.Button(value=f'{kind} list filtered', elem_id=f'{tabname}_{kind.lower()}_filter_indicator', elem_classes=['filter-indicator']) + indicator.click(fn=None, _js="() => openSettingsSection('sampler')", inputs=[], outputs=[], show_progress='hidden') + return indicator + + def create_sampler_and_steps_selection(choices, tabname, default_steps:int=20): if choices is None: sd_samplers.set_samplers() choices = [x for x in sd_samplers.samplers if not x.name == 'Same as primary'] + dropdown_choices, default_value, filtered = sampler_choices(choices) with gr.Row(elem_id=f"{tabname}_sampler_row", elem_classes=['flex-break', 'flexbox']): steps = gr.Slider(minimum=1, maximum=100, step=1, label="Steps", elem_id=f"{tabname}_steps", value=default_steps) - sampler_index = gr.Dropdown(label='Sampling method', elem_id=f"{tabname}_sampling", choices=[x.name for x in choices], value='Default', type="index") + create_filter_indicator(tabname, 'Sampler', filtered) + sampler_index = gr.Dropdown(label='Sampling method', elem_id=f"{tabname}_sampling", choices=dropdown_choices, value=default_value, type="value") return steps, sampler_index @@ -342,7 +371,9 @@ def create_hires_inputs(tab): with gr.Row(elem_id=f"{tab}_hires_fix_row2"): hr_force = gr.Checkbox(label='Force HiRes', value=False, elem_id=f"{tab}_hr_force") with gr.Row(elem_id=f"{tab}_hires_fix_row2"): - hr_sampler_index = gr.Dropdown(label='Refine sampler', elem_id=f"{tab}_sampling_alt", choices=[x.name for x in sd_samplers.samplers], value='Same as primary', type="index") + dropdown_choices, _default_value, filtered = sampler_choices(selected='Same as primary', same_as_primary=True) + create_filter_indicator(tab, 'Sampler', filtered) + hr_sampler_index = gr.Dropdown(label='Refine sampler', elem_id=f"{tab}_sampling_alt", choices=dropdown_choices, value='Same as primary', type="value") with gr.Row(elem_id=f"{tab}_hires_row2"): hr_second_pass_steps = gr.Slider(minimum=0, maximum=99, step=1, label='HiRes steps', elem_id=f"{tab}_steps_alt", value=20) denoising_strength = gr.Slider(minimum=0.0, maximum=0.99, step=0.01, label='Strength', value=0.3, elem_id=f"{tab}_denoising_strength") @@ -365,11 +396,24 @@ def create_resize_inputs(tab, images, accordion=True, latent=False, non_zero=Tru available_upscalers = ['None'] if not latent: available_upscalers = [x for x in available_upscalers if not x.lower().startswith('latent')] + available_upscalers, filtered = upscaler_choices(available_upscalers, available_upscalers[0]) resize_mode = gr.Dropdown(label=f"Mode{prefix}" if non_zero else "Resize mode", elem_id=f"{tab}_resize_mode", choices=shared.resize_modes, type="index", value='Fixed') + create_filter_indicator(tab, 'Upscaler', filtered) resize_name = gr.Dropdown(label=f"Method{prefix}" if non_zero else "Resize method", elem_id=f"{tab}_resize_name", choices=available_upscalers, value=available_upscalers[0], visible=True) resize_context_choices = ["Add with forward", "Remove with forward", "Add with backward", "Remove with backward"] resize_context = gr.Dropdown(label=f"Context{prefix}", elem_id=f"{tab}_resize_context", choices=resize_context_choices, value=resize_context_choices[0], visible=False) - resize_refresh_btn = ui_common.create_refresh_button(resize_name, modelloader.load_upscalers, lambda: {"choices": modelloader.load_upscalers()}, f'{tab}_upscalers_refresh') + + def refresh_upscaler_choices(selected): + modelloader.load_upscalers() + refreshed = [upscaler.name for upscaler in shared.sd_upscalers] + if not latent: + refreshed = [name for name in refreshed if not name.lower().startswith('latent')] + refreshed, _ = upscaler_choices(refreshed, selected) + value = selected if selected in refreshed else refreshed[0] + return gr.update(choices=refreshed, value=value) + + resize_refresh_btn = ToolButton(value=ui_symbols.refresh, elem_id=f'{tab}_upscalers_refresh') + resize_refresh_btn.click(fn=refresh_upscaler_choices, inputs=[resize_name], outputs=[resize_name], show_progress='hidden') def resize_mode_change(mode): if mode is None or mode == 0: diff --git a/modules/video_models/video_ui.py b/modules/video_models/video_ui.py index 74af572aa..07dafb2e5 100644 --- a/modules/video_models/video_ui.py +++ b/modules/video_models/video_ui.py @@ -60,8 +60,15 @@ def refresh_upscalers(): from modules import shared, modelloader modelloader.load_upscalers() # refresh upscalers = [u for u in shared.sd_upscalers if 'output_type' in inspect.signature(u.scaler.do_upscale).parameters.keys()] - upscaler_names = ['None'] + [u.name for u in upscalers] - return upscaler_names + return ['None'] + [u.name for u in upscalers] + + +def video_upscaler_choices(selected='None', refresh=False): + from modules import shared, modelloader + if refresh: + modelloader.load_upscalers() + upscalers = [upscaler for upscaler in shared.sd_upscalers if 'output_type' in inspect.signature(upscaler.scaler.do_upscale).parameters.keys()] + return ui_sections.upscaler_choices(['None'] + [upscaler.name for upscaler in upscalers], selected) def create_ui_outputs(): @@ -96,8 +103,17 @@ def create_ui_outputs(): with gr.Row(): upscale_scale = gr.Slider(label="Video scale", minimum=1, maximum=4, value=1, step=0.1, elem_id="video_outputs_upscale_scale") with gr.Row(): - upscale_upscaler = gr.Dropdown(label="Video Upscaler", choices=['None'], value='None', type='value', elem_id="video_outputs_upscale_upscaler") - _upscale_upscaler_btn = ui_common.create_refresh_button(upscale_upscaler, refresh_upscalers) + upscaler_names, filtered = video_upscaler_choices() + ui_sections.create_filter_indicator('video', 'Upscaler', filtered) + upscale_upscaler = gr.Dropdown(label="Video Upscaler", choices=upscaler_names, value='None', type='value', elem_id="video_outputs_upscale_upscaler") + + def refresh_video_upscalers(selected): + choices, _ = video_upscaler_choices(selected, refresh=True) + value = selected if selected in choices else choices[0] + return gr.update(choices=choices, value=value) + + _upscale_upscaler_btn = ToolButton(value=ui_symbols.refresh, elem_id='video_upscalers_refresh') + _upscale_upscaler_btn.click(fn=refresh_video_upscalers, inputs=[upscale_upscaler], outputs=[upscale_upscaler], show_progress='hidden') return mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, mp4_thumb, upscale_scale, upscale_upscaler diff --git a/test/test-ui-choice-filters.py b/test/test-ui-choice-filters.py new file mode 100644 index 000000000..7bb975a13 --- /dev/null +++ b/test/test-ui-choice-filters.py @@ -0,0 +1,37 @@ +"""CPU-only regression coverage for display-only sampler and upscaler filters.""" + +import pathlib +import sys +import unittest + +sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1])) +from modules.ui_choices import filter_ui_choices + + +class TestUiChoiceFilters(unittest.TestCase): + def setUp(self): + self.choices = ['Default', 'Euler', 'DPM++ 2M', 'Lanczos'] + + def test_empty_preferences_leave_choices_unfiltered(self): + visible, filtered = filter_ui_choices(self.choices, []) + self.assertEqual(visible, self.choices) + self.assertFalse(filtered) + + def test_preferences_filter_only_current_catalog_choices(self): + visible, filtered = filter_ui_choices(self.choices, ['Euler', 'Lanczos']) + self.assertEqual(visible, ['Euler', 'Lanczos']) + self.assertTrue(filtered) + + def test_stale_preferences_do_not_hide_the_catalog(self): + visible, filtered = filter_ui_choices(self.choices, ['Removed sampler']) + self.assertEqual(visible, self.choices) + self.assertFalse(filtered) + + def test_saved_selection_remains_available_when_not_preferred(self): + visible, filtered = filter_ui_choices(self.choices, ['Euler'], selected='DPM++ 2M') + self.assertEqual(visible, ['Euler', 'DPM++ 2M']) + self.assertTrue(filtered) + + +if __name__ == '__main__': + unittest.main() diff --git a/ui/globals.d.ts b/ui/globals.d.ts index 9def89c66..32f4fb4e5 100644 --- a/ui/globals.d.ts +++ b/ui/globals.d.ts @@ -141,6 +141,7 @@ declare global { onUiUpdate?: (callback: () => void) => void; // ui/script.ts timer?: (name: string, elapsed: number) => Promise; // ui/timers.ts markIfModified?: (setting_name: string, value: unknown) => void; // ui/settings.ts + openSettingsSection?: (sectionId: string) => void; // ui/settings.ts appendContextMenuOption?: (targetElementSelector: string, entryName: string, entryFunction: () => void, primary?: boolean) => string; // ui/contextMenus.ts generateForever?: (genbuttonid: string) => void; // ui/contextMenus.ts removeContextMenuOption?: (id: string) => void; // ui/contextMenus.ts diff --git a/ui/locale/locale_en.json b/ui/locale/locale_en.json index 4e84f1f34..47bb4dd2d 100644 --- a/ui/locale/locale_en.json +++ b/ui/locale/locale_en.json @@ -1275,6 +1275,8 @@ ], "s": [ {"id":"txt2img_sampler","label":"Sampler","localized":"","hint":"Settings related to sampler and seed selection and configuration. Samplers guide the process of turning noise into an image over multiple steps.","ui":"txt2img"}, + {"id":"","label":"Sampler list filtered","localized":"","hint":"This list only shows samplers selected in Sampler Settings. Select to open settings.","ui":"txt2img"}, + {"id":"","label":"Sampler Settings","localized":"","hint":"Preferences for sampler and upscaler lists.","ui":"settings_sampler"}, {"id":"","label":"Scripts","localized":"","hint":"Enable additional features by using selected scripts during generate process","ui":"txt2img"}, {"id":"","label":"Scale","localized":"","hint":"Resize image to target scale. If resize fixed width/height are set this option is ignored","ui":"txt2img"}, {"id":"xy_grid_swap_axes_button","label":"Swap X/Y","localized":"","hint":"","ui":"script_xyz_grid_script"}, @@ -1325,6 +1327,8 @@ {"id":"","label":"Server log","localized":"","hint":""}, {"id":"","label":"Steps","localized":"","hint":"How many times to improve the generated image iteratively; higher values take longer; very low values can produce bad results","ui":"txt2img"}, {"id":"","label":"Sampling method","localized":"","hint":"Which algorithm to use to produce the image","ui":"txt2img"}, + {"id":"","label":"Show samplers in user interface","localized":"","hint":"Select favorite samplers to show in generated dropdowns. Leave empty to show all samplers. Restart the UI after changing this setting.","ui":"settings_sampler"}, + {"id":"","label":"Show upscalers in user interface","localized":"","hint":"Select favorite upscalers to show in generated dropdowns. Leave empty to show all upscalers. Restart the UI after changing this setting.","ui":"settings_sampler"}, {"id":"","label":"Sigma method","localized":"","hint":"Controls how noise levels (sigmas) are distributed across diffusion steps.
Default: use the scheduler's built-in sigma method.
Karras: smoother schedule that emphasizes later steps where fine details emerge; generally higher quality with fewer steps.
Betas: derive sigmas directly from the model's beta schedule (classic DDPM behavior).
Exponential: exponential decay of noise across steps; aggressive denoising early, slower refinement later.
Lambdas: Lu's lambdas method from the DPM-Solver paper, specific to the DPM++ family.
Flowmatch: sigma schedule tuned for flow-matching models (Flux, SD3, video models).","ui":"txt2img"}, {"id":"","label":"Sigma adjust","localized":"","hint":"Multiplier applied to the sampler's step size during the active timestep window. (Sigma is the amount of noise the sampler removes at each step.)
Values below 1.0 shrink the step for smoother, more conservative denoising. Values above 1.0 enlarge it for sharper, more aggressive sampling.

Default 1.0 disables the adjustment entirely. Use Adjust start and Adjust end to define the timestep range where the multiplier takes effect.","ui":"txt2img"}, {"id":"","label":"Sampler order","localized":"","hint":"Overrides the solver order of the active sampler when set above 0.
Higher orders use more historical steps per update for greater stability and accuracy at the cost of extra compute. Lower orders are faster but noisier.

Default 0 leaves each sampler at its built-in order. Many samplers in the dropdown already encode their order in the name (e.g. DPM++ 2M is order 2, DPM++ 3M is order 3, DPM++ 2M SDE is order 2).

Within a sampler family, the named variants differ ONLY by this value, so picking DPM++ 2M with the slider at 3 produces a scheduler that is functionally identical to picking DPM++ 3M with the slider at 0. The same equivalence holds across the rest of the DPM++ multistep family (including the SDE and Inverse variants) and across the ER-SDE family.

Samplers without a configurable solver order (DDIM, plain Euler, ancestrals, etc.) ignore this slider entirely.","ui":"txt2img"}, @@ -1577,6 +1581,7 @@ {"id":"component-5611","label":"Update all","localized":"","hint":"","ui":"models_metadata_tab"}, {"id":"","label":"UNet/DiT","localized":"","hint":""}, {"id":"","label":"Upscale","localized":"","hint":"Upscale image","ui":"extras"}, + {"id":"","label":"Upscaler list filtered","localized":"","hint":"This list only shows upscalers selected in Sampler Settings. Select to open settings.","ui":"txt2img"}, {"id":"","label":"UI Tabs","localized":"","hint":"","ui":"settings_ui"}, {"id":"","label":"Upscaling","localized":"","hint":"","ui":"settings_postprocessing"}, {"id":"","label":"Use segmentation","localized":"","hint":"Use the model's pixel-precise segmentation mask as the inpaint mask instead of the rectangular bounding box.
Tighter mask means less unintended change around the detection (e.g., the inpaint stays on the face, not on the hair or background behind it). Better blending and smaller seams.

Requires a segmentation-capable model (filename usually contains -seg). Bounding-box-only models silently fall back to the rectangle.
Default off.","ui":"txt2img"}, diff --git a/ui/settings.ts b/ui/settings.ts index 7f220b65c..7ef5b5a3a 100644 --- a/ui/settings.ts +++ b/ui/settings.ts @@ -78,6 +78,17 @@ function showAllSettings() { }); } +function openSettingsSection(sectionId: string) { + const settingsTab = gradioApp().getElementById('tab_settings'); + const settingsButton = settingsTab ? gradioApp().querySelector(`button[aria-controls="${settingsTab.id}"]`) : null; + settingsButton?.click(); + const section = gradioApp().getElementById(`settings_section_tab_${sectionId}`); + const sectionButton = section ? gradioApp().querySelector(`button[aria-controls="${section.id}"]`) : null; + sectionButton?.click(); + section?.scrollIntoView({ behavior: 'smooth', block: 'start' }); +} +window.openSettingsSection = openSettingsSection; + function markIfModified(setting_name, value) { if (!opts_metadata[setting_name]) return; const elem = gradioApp().getElementById(`modification_indicator_${setting_name}`);