ui_extensions.py 23.8 KB
Newer Older
1 2 3 4
import json
import os.path
import sys
import time
5
from datetime import datetime
6 7 8 9 10 11
import traceback

import git

import gradio as gr
import html
12 13
import shutil
import errno
14

15 16
from modules import extensions, shared, paths, config_states
from modules.paths_internal import config_states_dir
17
from modules.call_queue import wrap_gradio_gpu_call
18

A
AUTOMATIC 已提交
19
available_extensions = {"extensions": []}
20
STYLE_PRIMARY = ' style="color: var(--primary-400)"'
A
AUTOMATIC 已提交
21 22


23
def check_access():
J
jcowens 已提交
24
    assert not shared.cmd_opts.disable_extension_access, "extension access disabled because of command line flags"
25 26


27
def apply_and_restart(disable_list, update_list, disable_all):
28 29
    check_access()

30 31 32 33 34 35
    disabled = json.loads(disable_list)
    assert type(disabled) == list, f"wrong disable_list data for apply_and_restart: {disable_list}"

    update = json.loads(update_list)
    assert type(update) == list, f"wrong update_list data for apply_and_restart: {update_list}"

36 37 38
    if update:
        save_config_state("Backup (pre-update)")

39 40 41 42 43 44 45
    update = set(update)

    for ext in extensions.extensions:
        if ext.name not in update:
            continue

        try:
46
            ext.fetch_and_reset_hard()
47
        except Exception:
48
            print(f"Error getting updates for {ext.name}:", file=sys.stderr)
49 50 51
            print(traceback.format_exc(), file=sys.stderr)

    shared.opts.disabled_extensions = disabled
52
    shared.opts.disable_all_extensions = disable_all
53 54 55 56 57 58
    shared.opts.save(shared.config_filename)

    shared.state.interrupt()
    shared.state.need_restart = True


59 60 61 62 63
def save_config_state(name):
    current_config_state = config_states.get_config()
    if not name:
        name = "Config"
    current_config_state["name"] = name
64
    filename = os.path.join(config_states_dir, datetime.now().strftime("%Y_%m_%d-%H_%M_%S") + "_" + name + ".json")
65 66 67 68 69 70
    print(f"Saving backup of webui/extension state to {filename}.")
    with open(filename, "w", encoding="utf-8") as f:
        json.dump(current_config_state, f)
    config_states.list_config_states()
    new_value = next(iter(config_states.all_config_states.keys()), "Current")
    new_choices = ["Current"] + list(config_states.all_config_states.keys())
71
    return gr.Dropdown.update(value=new_value, choices=new_choices), f"<span>Saved current webui/extension state to \"{filename}\"</span>"
72 73 74 75 76 77 78 79 80 81 82 83


def restore_config_state(confirmed, config_state_name, restore_type):
    if config_state_name == "Current":
        return "<span>Select a config to restore from.</span>"
    if not confirmed:
        return "<span>Cancelled.</span>"

    check_access()

    config_state = config_states.all_config_states[config_state_name]

84
    print(f"*** Restoring webui state from backup: {restore_type} ***")
85 86

    if restore_type == "extensions" or restore_type == "both":
87
        shared.opts.restore_config_state_file = config_state["filepath"]
88 89 90 91 92 93 94 95 96 97 98
        shared.opts.save(shared.config_filename)

    if restore_type == "webui" or restore_type == "both":
        config_states.restore_webui_config(config_state)

    shared.state.interrupt()
    shared.state.need_restart = True

    return ""


99
def check_updates(id_task, disable_list):
100 101
    check_access()

102 103 104 105 106 107 108 109
    disabled = json.loads(disable_list)
    assert type(disabled) == list, f"wrong disable_list data for apply_and_restart: {disable_list}"

    exts = [ext for ext in extensions.extensions if ext.remote is not None and ext.name not in disabled]
    shared.state.job_count = len(exts)

    for ext in exts:
        shared.state.textinfo = ext.name
110 111 112

        try:
            ext.check_updates()
113 114 115
        except FileNotFoundError as e:
            if 'FETCH_HEAD' not in str(e):
                raise
116 117 118 119
        except Exception:
            print(f"Error checking updates for {ext.name}:", file=sys.stderr)
            print(traceback.format_exc(), file=sys.stderr)

120 121 122
        shared.state.nextjob()

    return extension_table(), ""
123 124


125 126 127 128 129 130 131 132 133 134
def make_commit_link(commit_hash, remote, text=None):
    if text is None:
        text = commit_hash[:8]
    if remote.startswith("https://github.com/"):
        href = os.path.join(remote, "commit", commit_hash)
        return f'<a href="{href}" target="_blank">{text}</a>'
    else:
        return text


135 136 137 138 139 140 141
def extension_table():
    code = f"""<!-- {time.time()} -->
    <table id="extensions">
        <thead>
            <tr>
                <th><abbr title="Use checkbox to enable the extension; it will be enabled or disabled when you click apply button">Extension</abbr></th>
                <th>URL</th>
142
                <th><abbr title="Extension version">Version</abbr></th>
143 144 145 146 147 148 149
                <th><abbr title="Use checkbox to mark the extension for update; it will be updated when you click apply button">Update</abbr></th>
            </tr>
        </thead>
        <tbody>
    """

    for ext in extensions.extensions:
150 151
        ext.read_info_from_repo()

152
        remote = f"""<a href="{html.escape(ext.remote or '')}" target="_blank">{html.escape("built-in" if ext.is_builtin else ext.remote or '')}</a>"""
A
AUTOMATIC 已提交
153

154 155 156 157 158
        if ext.can_update:
            ext_status = f"""<label><input class="gr-check-radio gr-checkbox" name="update_{html.escape(ext.name)}" checked="checked" type="checkbox">{html.escape(ext.status)}</label>"""
        else:
            ext_status = ext.status

159 160
        style = ""
        if shared.opts.disable_all_extensions == "extra" and not ext.is_builtin or shared.opts.disable_all_extensions == "all":
161
            style = STYLE_PRIMARY
162

163 164 165 166
        version_link = ext.version
        if ext.commit_hash and ext.remote:
            version_link = make_commit_link(ext.commit_hash, ext.remote, ext.version)

167 168
        code += f"""
            <tr>
169
                <td><label{style}><input class="gr-check-radio gr-checkbox" name="enable_{html.escape(ext.name)}" type="checkbox" {'checked="checked"' if ext.enabled else ''}>{html.escape(ext.name)}</label></td>
A
AUTOMATIC 已提交
170
                <td>{remote}</td>
171
                <td>{version_link}</td>
172 173 174 175 176 177 178 179 180 181 182 183
                <td{' class="extension_status"' if ext.remote is not None else ''}>{ext_status}</td>
            </tr>
    """

    code += """
        </tbody>
    </table>
    """

    return code


184 185 186 187 188 189 190 191
def update_config_states_table(state_name):
    if state_name == "Current":
        config_state = config_states.get_config()
    else:
        config_state = config_states.all_config_states[state_name]

    config_name = config_state.get("name", "Config")
    created_date = time.asctime(time.gmtime(config_state["created_at"]))
192
    filepath = config_state.get("filepath", "<unknown>")
193 194 195 196 197

    code = f"""<!-- {time.time()} -->"""

    webui_remote = config_state["webui"]["remote"] or ""
    webui_branch = config_state["webui"]["branch"]
198
    webui_commit_hash = config_state["webui"]["commit_hash"] or "<unknown>"
199 200 201 202 203 204
    webui_commit_date = config_state["webui"]["commit_date"]
    if webui_commit_date:
        webui_commit_date = time.asctime(time.gmtime(webui_commit_date))
    else:
        webui_commit_date = "<unknown>"

205 206 207 208 209 210 211 212 213 214 215 216
    current_webui = config_states.get_webui_config()

    style_remote = ""
    style_branch = ""
    style_commit = ""
    if current_webui["remote"] != webui_remote:
        style_remote = STYLE_PRIMARY
    if current_webui["branch"] != webui_branch:
        style_branch = STYLE_PRIMARY
    if current_webui["commit_hash"] != webui_commit_hash:
        style_commit = STYLE_PRIMARY

217 218 219
    commit_link = make_commit_link(webui_commit_hash, webui_remote)
    date_link = make_commit_link(webui_commit_hash, webui_remote, webui_commit_date)

220
    code += f"""<h2>Config Backup: {config_name}</h2>
S
space-nuko 已提交
221 222
      <div><b>Filepath:</b> {filepath}</div>
      <div><b>Created at:</b> {created_date}</div>"""
223 224 225 226 227 228 229 230 231 232 233 234 235

    code += f"""<h2>WebUI State</h2>
      <table id="config_state_webui">
        <thead>
            <tr>
                <th>URL</th>
                <th>Branch</th>
                <th>Commit</th>
                <th>Date</th>
            </tr>
        </thead>
        <tbody>
            <tr>
236 237
                <td><label{style_remote}>{webui_remote}</label></td>
                <td><label{style_branch}>{webui_branch}</label></td>
238 239
                <td><label{style_commit}>{commit_link}</label></td>
                <td><label{style_commit}>{date_link}</label></td>
240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281
            </tr>
        </tbody>
      </table>
    """

    code += """<h2>Extension State</h2>
      <table id="config_state_extensions">
        <thead>
            <tr>
                <th>Extension</th>
                <th>URL</th>
                <th>Branch</th>
                <th>Commit</th>
                <th>Date</th>
            </tr>
        </thead>
        <tbody>
    """

    ext_map = {ext.name: ext for ext in extensions.extensions}

    for ext_name, ext_conf in config_state["extensions"].items():
        ext_remote = ext_conf["remote"] or ""
        ext_branch = ext_conf["branch"] or "<unknown>"
        ext_enabled = ext_conf["enabled"]
        ext_commit_hash = ext_conf["commit_hash"] or "<unknown>"
        ext_commit_date = ext_conf["commit_date"]
        if ext_commit_date:
            ext_commit_date = time.asctime(time.gmtime(ext_commit_date))
        else:
            ext_commit_date = "<unknown>"

        remote = f"""<a href="{html.escape(ext_remote)}" target="_blank">{html.escape(ext_remote or '')}</a>"""

        style_enabled = ""
        style_remote = ""
        style_branch = ""
        style_commit = ""
        if ext_name in ext_map:
            current_ext = ext_map[ext_name]
            current_ext.read_info_from_repo()
            if current_ext.enabled != ext_enabled:
282
                style_enabled = STYLE_PRIMARY
283
            if current_ext.remote != ext_remote:
284
                style_remote = STYLE_PRIMARY
285
            if current_ext.branch != ext_branch:
286
                style_branch = STYLE_PRIMARY
287
            if current_ext.commit_hash != ext_commit_hash:
288
                style_commit = STYLE_PRIMARY
289

290 291 292
            commit_link = make_commit_link(ext_commit_hash, ext_remote)
            date_link = make_commit_link(ext_commit_hash, ext_remote, ext_commit_date)

293 294 295 296 297
        code += f"""
            <tr>
                <td><label{style_enabled}><input class="gr-check-radio gr-checkbox" type="checkbox" disabled="true" {'checked="checked"' if ext_enabled else ''}>{html.escape(ext_name)}</label></td>
                <td><label{style_remote}>{remote}</label></td>
                <td><label{style_branch}>{ext_branch}</label></td>
298 299
                <td><label{style_commit}>{commit_link}</label></td>
                <td><label{style_commit}>{date_link}</label></td>
300 301 302 303 304 305 306 307 308 309 310
            </tr>
    """

    code += """
        </tbody>
    </table>
    """

    return code


A
AUTOMATIC 已提交
311 312 313 314 315 316 317 318
def normalize_git_url(url):
    if url is None:
        return ""

    url = url.replace(".git", "")
    return url


319
def install_extension_from_url(dirname, url):
320 321
    check_access()

322 323 324 325
    assert url, 'No URL specified'

    if dirname is None or dirname == "":
        *parts, last_part = url.split('/')
A
AUTOMATIC 已提交
326
        last_part = normalize_git_url(last_part)
327 328 329 330 331 332

        dirname = last_part

    target_dir = os.path.join(extensions.extensions_dir, dirname)
    assert not os.path.exists(target_dir), f'Extension directory already exists: {target_dir}'

A
AUTOMATIC 已提交
333 334
    normalized_url = normalize_git_url(url)
    assert len([x for x in extensions.extensions if normalize_git_url(x.remote) == normalized_url]) == 0, 'Extension with this URL is already installed'
335

336
    tmpdir = os.path.join(paths.data_path, "tmp", dirname)
337 338 339

    try:
        shutil.rmtree(tmpdir, True)
F
Ftps 已提交
340 341 342 343
        with git.Repo.clone_from(url, tmpdir) as repo:
            repo.remote().fetch()
            for submodule in repo.submodules:
                submodule.update()
344 345 346 347 348 349 350 351 352
        try:
            os.rename(tmpdir, target_dir)
        except OSError as err:
            if err.errno == errno.EXDEV:
                # Cross device link, typical in docker or when tmp/ and extensions/ are on different file systems
                # Since we can't use a rename, do the slower but more versitile shutil.move()
                shutil.move(tmpdir, target_dir)
            else:
                # Something else, not enough free space, permissions, etc.  rethrow it so that it gets handled.
F
Ftps 已提交
353
                raise err
354

355 356 357
        import launch
        launch.run_extension_installer(target_dir)

358 359 360 361 362 363
        extensions.list_extensions()
        return [extension_table(), html.escape(f"Installed into {target_dir}. Use Installed tab to restart.")]
    finally:
        shutil.rmtree(tmpdir, True)


364
def install_extension_from_index(url, hide_tags, sort_column, filter_text):
A
AUTOMATIC 已提交
365 366
    ext_table, message = install_extension_from_url(None, url)

367
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text)
A
AUTOMATIC 已提交
368

369
    return code, ext_table, message, ''
A
AUTOMATIC 已提交
370

371

372
def refresh_available_extensions(url, hide_tags, sort_column):
A
AUTOMATIC 已提交
373 374 375 376 377 378 379 380
    global available_extensions

    import urllib.request
    with urllib.request.urlopen(url) as response:
        text = response.read()

    available_extensions = json.loads(text)

381
    code, tags = refresh_available_extensions_from_data(hide_tags, sort_column)
382

383
    return url, code, gr.CheckboxGroup.update(choices=tags), '', ''
384 385


386 387 388 389 390 391 392 393
def refresh_available_extensions_for_tags(hide_tags, sort_column, filter_text):
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text)

    return code, ''


def search_extensions(filter_text, hide_tags, sort_column):
    code, _ = refresh_available_extensions_from_data(hide_tags, sort_column, filter_text)
A
AUTOMATIC 已提交
394

395
    return code, ''
A
AUTOMATIC 已提交
396

397

398 399 400 401 402 403 404 405 406 407
sort_ordering = [
    # (reverse, order_by_function)
    (True, lambda x: x.get('added', 'z')),
    (False, lambda x: x.get('added', 'z')),
    (False, lambda x: x.get('name', 'z')),
    (True, lambda x: x.get('name', 'z')),
    (False, lambda x: 'z'),
]


408
def refresh_available_extensions_from_data(hide_tags, sort_column, filter_text=""):
A
AUTOMATIC 已提交
409 410 411
    extlist = available_extensions["extensions"]
    installed_extension_urls = {normalize_git_url(extension.remote): extension.name for extension in extensions.extensions}

412 413 414 415
    tags = available_extensions.get("tags", {})
    tags_to_hide = set(hide_tags)
    hidden = 0

A
AUTOMATIC 已提交
416 417 418 419 420 421 422 423 424 425 426 427
    code = f"""<!-- {time.time()} -->
    <table id="available_extensions">
        <thead>
            <tr>
                <th>Extension</th>
                <th>Description</th>
                <th>Action</th>
            </tr>
        </thead>
        <tbody>
    """

428 429 430
    sort_reverse, sort_function = sort_ordering[sort_column if 0 <= sort_column < len(sort_ordering) else 0]

    for ext in sorted(extlist, key=sort_function, reverse=sort_reverse):
A
AUTOMATIC 已提交
431
        name = ext.get("name", "noname")
432
        added = ext.get('added', 'unknown')
A
AUTOMATIC 已提交
433 434
        url = ext.get("url", None)
        description = ext.get("description", "")
435
        extension_tags = ext.get("tags", [])
A
AUTOMATIC 已提交
436 437 438 439

        if url is None:
            continue

A
AUTOMATIC 已提交
440 441 442
        existing = installed_extension_urls.get(normalize_git_url(url), None)
        extension_tags = extension_tags + ["installed"] if existing else extension_tags

443 444 445 446
        if len([x for x in extension_tags if x in tags_to_hide]) > 0:
            hidden += 1
            continue

447 448 449 450 451
        if filter_text and filter_text.strip():
            if filter_text.lower() not in html.escape(name).lower() and filter_text.lower() not in html.escape(description).lower():
                hidden += 1
                continue

A
AUTOMATIC 已提交
452
        install_code = f"""<button onclick="install_extension_from_index(this, '{html.escape(url)}')" {"disabled=disabled" if existing else ""} class="lg secondary gradio-button custom-button">{"Install" if not existing else "Installed"}</button>"""
A
AUTOMATIC 已提交
453

454 455
        tags_text = ", ".join([f"<span class='extension-tag' title='{tags.get(x, '')}'>{x}</span>" for x in extension_tags])

A
AUTOMATIC 已提交
456 457
        code += f"""
            <tr>
458
                <td><a href="{html.escape(url)}" target="_blank">{html.escape(name)}</a><br />{tags_text}</td>
459
                <td>{html.escape(description)}<p class="info"><span class="date_added">Added: {html.escape(added)}</span></p></td>
A
AUTOMATIC 已提交
460 461
                <td>{install_code}</td>
            </tr>
A
AUTOMATIC 已提交
462 463 464 465 466
        
        """

        for tag in [x for x in extension_tags if x not in tags]:
            tags[tag] = tag
A
AUTOMATIC 已提交
467 468 469 470 471 472

    code += """
        </tbody>
    </table>
    """

473 474 475 476
    if hidden > 0:
        code += f"<p>Extension hidden: {hidden}</p>"

    return code, list(tags)
A
AUTOMATIC 已提交
477 478


479 480 481
def create_ui():
    import modules.ui

482 483
    config_states.list_config_states()

484 485 486 487
    with gr.Blocks(analytics_enabled=False) as ui:
        with gr.Tabs(elem_id="tabs_extensions") as tabs:
            with gr.TabItem("Installed"):

488
                with gr.Row(elem_id="extensions_installed_top"):
489 490
                    apply = gr.Button(value="Apply and restart UI", variant="primary")
                    check = gr.Button(value="Check for updates")
491
                    extensions_disable_all = gr.Radio(label="Disable all extensions", choices=["none", "extra", "all"], value=shared.opts.disable_all_extensions, elem_id="extensions_disable_all")
A
AUTOMATIC 已提交
492 493
                    extensions_disabled_list = gr.Text(elem_id="extensions_disabled_list", visible=False).style(container=False)
                    extensions_update_list = gr.Text(elem_id="extensions_update_list", visible=False).style(container=False)
494

495 496 497 498 499 500 501 502
                html = ""
                if shared.opts.disable_all_extensions != "none":
                    html = """
<span style="color: var(--primary-400);">
    "Disable all extensions" was set, change it to "none" to load all extensions again
</span>
                    """
                info = gr.HTML(html)
503 504 505 506 507
                extensions_table = gr.HTML(lambda: extension_table())

                apply.click(
                    fn=apply_and_restart,
                    _js="extensions_apply",
508
                    inputs=[extensions_disabled_list, extensions_update_list, extensions_disable_all],
509 510 511 512
                    outputs=[],
                )

                check.click(
513
                    fn=wrap_gradio_gpu_call(check_updates, extra_outputs=[gr.update()]),
514
                    _js="extensions_check",
515 516
                    inputs=[info, extensions_disabled_list],
                    outputs=[extensions_table, info],
517 518
                )

A
AUTOMATIC 已提交
519 520 521
            with gr.TabItem("Available"):
                with gr.Row():
                    refresh_available_extensions_button = gr.Button(value="Load from:", variant="primary")
522
                    available_extensions_index = gr.Text(value="https://raw.githubusercontent.com/AUTOMATIC1111/stable-diffusion-webui-extensions/master/index.json", label="Extension index URL").style(container=False)
A
AUTOMATIC 已提交
523 524 525
                    extension_to_install = gr.Text(elem_id="extension_to_install", visible=False)
                    install_extension_button = gr.Button(elem_id="install_extension_button", visible=False)

526
                with gr.Row():
A
AUTOMATIC 已提交
527
                    hide_tags = gr.CheckboxGroup(value=["ads", "localization", "installed"], label="Hide extensions with tags", choices=["script", "ads", "localization", "installed"])
528
                    sort_column = gr.Radio(value="newest first", label="Order", choices=["newest first", "oldest first", "a-z", "z-a", "internal order", ], type="index")
529

530 531 532
                with gr.Row(): 
                    search_extensions_text = gr.Text(label="Search").style(container=False)
                   
A
AUTOMATIC 已提交
533 534 535 536
                install_result = gr.HTML()
                available_extensions_table = gr.HTML()

                refresh_available_extensions_button.click(
537
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions, extra_outputs=[gr.update(), gr.update(), gr.update()]),
538
                    inputs=[available_extensions_index, hide_tags, sort_column],
539
                    outputs=[available_extensions_index, available_extensions_table, hide_tags, install_result, search_extensions_text],
A
AUTOMATIC 已提交
540 541 542 543
                )

                install_extension_button.click(
                    fn=modules.ui.wrap_gradio_call(install_extension_from_index, extra_outputs=[gr.update(), gr.update()]),
544
                    inputs=[extension_to_install, hide_tags, sort_column, search_extensions_text],
A
AUTOMATIC 已提交
545 546 547
                    outputs=[available_extensions_table, extensions_table, install_result],
                )

548 549 550 551 552 553
                search_extensions_text.change(
                    fn=modules.ui.wrap_gradio_call(search_extensions, extra_outputs=[gr.update()]),
                    inputs=[search_extensions_text, hide_tags, sort_column],
                    outputs=[available_extensions_table, install_result],
                )

554 555
                hide_tags.change(
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]),
556
                    inputs=[hide_tags, sort_column, search_extensions_text],
557 558 559 560 561
                    outputs=[available_extensions_table, install_result]
                )

                sort_column.change(
                    fn=modules.ui.wrap_gradio_call(refresh_available_extensions_for_tags, extra_outputs=[gr.update()]),
562
                    inputs=[hide_tags, sort_column, search_extensions_text],
563 564 565
                    outputs=[available_extensions_table, install_result]
                )

566 567 568
            with gr.TabItem("Install from URL"):
                install_url = gr.Text(label="URL for extension's git repository")
                install_dirname = gr.Text(label="Local directory name", placeholder="Leave empty for auto")
A
AUTOMATIC 已提交
569 570
                install_button = gr.Button(value="Install", variant="primary")
                install_result = gr.HTML(elem_id="extension_install_result")
571

A
AUTOMATIC 已提交
572
                install_button.click(
573 574
                    fn=modules.ui.wrap_gradio_call(install_extension_from_url, extra_outputs=[gr.update()]),
                    inputs=[install_dirname, install_url],
A
AUTOMATIC 已提交
575
                    outputs=[extensions_table, install_result],
576 577
                )

578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601
            with gr.TabItem("Backup/Restore"):
                with gr.Row(elem_id="extensions_backup_top_row"):
                    config_states_list = gr.Dropdown(label="Saved Configs", elem_id="extension_backup_saved_configs", value="Current", choices=["Current"] + list(config_states.all_config_states.keys()))
                    modules.ui.create_refresh_button(config_states_list, config_states.list_config_states, lambda: {"choices": ["Current"] + list(config_states.all_config_states.keys())}, "refresh_config_states")
                    config_restore_type = gr.Radio(label="State to restore", choices=["extensions", "webui", "both"], value="extensions", elem_id="extension_backup_restore_type")
                    config_restore_button = gr.Button(value="Restore Selected Config", variant="primary", elem_id="extension_backup_restore")
                with gr.Row(elem_id="extensions_backup_top_row2"):
                    config_save_name = gr.Textbox("", placeholder="Config Name", show_label=False)
                    config_save_button = gr.Button(value="Save Current Config")

                config_states_info = gr.HTML("")
                config_states_table = gr.HTML(lambda: update_config_states_table("Current"))

                config_save_button.click(fn=save_config_state, inputs=[config_save_name], outputs=[config_states_list, config_states_info])

                dummy_component = gr.Label(visible=False)
                config_restore_button.click(fn=restore_config_state, _js="config_state_confirm_restore", inputs=[dummy_component, config_states_list, config_restore_type], outputs=[config_states_info])

                config_states_list.change(
                    fn=update_config_states_table,
                    inputs=[config_states_list],
                    outputs=[config_states_table],
                )

602
    return ui