text-generation-webui/modules/ui.py

550 lines
18 KiB
Python
Raw Normal View History

import copy
2025-06-07 22:46:52 -03:00
import threading
2023-03-15 12:33:26 -03:00
from pathlib import Path
import gradio as gr
import yaml
import extensions
import modules.extensions as extensions_module
2023-04-12 10:27:06 -03:00
from modules import shared
from modules.chat import load_history
2025-06-07 22:46:52 -03:00
from modules.utils import gradio
# Global state for auto-saving UI settings with debouncing
_auto_save_timer = None
_auto_save_lock = threading.Lock()
_last_interface_state = None
_last_preset = None
_last_extensions = None
_last_show_controls = None
_last_theme_state = None
2023-04-12 10:27:06 -03:00
with open(Path(__file__).resolve().parent / '../css/NotoSans/stylesheet.css', 'r', encoding='utf-8') as f:
2023-03-15 12:33:26 -03:00
css = f.read()
with open(Path(__file__).resolve().parent / '../css/main.css', 'r', encoding='utf-8') as f:
css += f.read()
with open(Path(__file__).resolve().parent / '../css/katex/katex.min.css', 'r', encoding='utf-8') as f:
css += f.read()
with open(Path(__file__).resolve().parent / '../css/highlightjs/highlightjs-copy.min.css', 'r', encoding='utf-8') as f:
css += f.read()
with open(Path(__file__).resolve().parent / '../js/main.js', 'r', encoding='utf-8') as f:
2023-08-13 01:12:15 -03:00
js = f.read()
with open(Path(__file__).resolve().parent / '../js/global_scope_js.js', 'r', encoding='utf-8') as f:
global_scope_js = f.read()
with open(Path(__file__).resolve().parent / '../js/save_files.js', 'r', encoding='utf-8') as f:
save_files_js = f.read()
with open(Path(__file__).resolve().parent / '../js/switch_tabs.js', 'r', encoding='utf-8') as f:
switch_tabs_js = f.read()
with open(Path(__file__).resolve().parent / '../js/show_controls.js', 'r', encoding='utf-8') as f:
show_controls_js = f.read()
with open(Path(__file__).resolve().parent / '../js/update_big_picture.js', 'r', encoding='utf-8') as f:
update_big_picture_js = f.read()
with open(Path(__file__).resolve().parent / '../js/dark_theme.js', 'r', encoding='utf-8') as f:
dark_theme_js = f.read()
refresh_symbol = '🔄'
delete_symbol = '🗑️'
save_symbol = '💾'
2023-04-18 23:36:23 -03:00
theme = gr.themes.Default(
font=['Noto Sans', 'Helvetica', 'ui-sans-serif', 'system-ui', 'sans-serif'],
2023-04-18 23:36:23 -03:00
font_mono=['IBM Plex Mono', 'ui-monospace', 'Consolas', 'monospace'],
).set(
border_color_primary='#c5c5d2',
button_large_padding='6px 12px',
2023-04-21 02:47:18 -03:00
body_text_color_subdued='#484848',
2024-03-13 08:18:49 -07:00
background_fill_secondary='#eaeaea',
background_fill_primary='var(--neutral-50)',
2024-06-27 20:47:42 -07:00
body_background_fill="white",
block_background_fill="#f4f4f4",
body_text_color="#333",
button_secondary_background_fill="#f4f4f4",
button_secondary_border_color="var(--border-color-primary)"
2023-04-18 23:36:23 -03:00
)
2024-12-17 00:47:41 -03:00
if not shared.args.old_colors:
theme = theme.set(
# General Colors
border_color_primary='#c5c5d2',
body_text_color_subdued='#484848',
background_fill_secondary='#eaeaea',
2025-06-19 11:28:12 -07:00
background_fill_secondary_dark='var(--selected-item-color-dark, #282930)',
2024-12-17 00:47:41 -03:00
background_fill_primary='var(--neutral-50)',
2025-06-19 11:28:12 -07:00
background_fill_primary_dark='var(--darker-gray, #1C1C1D)',
2024-12-17 00:47:41 -03:00
body_background_fill="white",
block_background_fill="transparent",
2025-05-05 06:16:11 -07:00
body_text_color='rgb(64, 64, 64)',
2025-06-08 15:19:25 -07:00
button_secondary_background_fill="white",
2024-12-17 00:47:41 -03:00
button_secondary_border_color="var(--border-color-primary)",
2025-06-08 15:19:25 -07:00
input_shadow="none",
button_shadow_hover="none",
2024-12-17 00:47:41 -03:00
# Dark Mode Colors
2025-06-19 11:28:12 -07:00
input_background_fill_dark='var(--darker-gray, #1C1C1D)',
checkbox_background_color_dark='var(--darker-gray, #1C1C1D)',
2024-12-17 00:47:41 -03:00
block_background_fill_dark='transparent',
block_border_color_dark='transparent',
2025-06-19 11:28:12 -07:00
input_border_color_dark='var(--border-color-dark, #525252)',
input_border_color_focus_dark='var(--border-color-dark, #525252)',
checkbox_border_color_dark='var(--border-color-dark, #525252)',
border_color_primary_dark='var(--border-color-dark, #525252)',
button_secondary_border_color_dark='var(--border-color-dark, #525252)',
body_background_fill_dark='var(--dark-gray, #212125)',
2024-12-17 00:47:41 -03:00
button_primary_background_fill_dark='transparent',
button_secondary_background_fill_dark='transparent',
checkbox_label_background_fill_dark='transparent',
button_cancel_background_fill_dark='transparent',
2025-06-19 11:28:12 -07:00
button_secondary_background_fill_hover_dark='var(--selected-item-color-dark, #282930)',
checkbox_label_background_fill_hover_dark='var(--selected-item-color-dark, #282930)',
table_even_background_fill_dark='var(--darker-gray, #1C1C1D)',
table_odd_background_fill_dark='var(--selected-item-color-dark, #282930)',
code_background_fill_dark='var(--darker-gray, #1C1C1D)',
2024-12-17 00:47:41 -03:00
# Shadows and Radius
checkbox_label_shadow='none',
block_shadow='none',
block_shadow_dark='none',
2025-05-30 11:32:24 -07:00
input_shadow_focus='none',
input_shadow_focus_dark='none',
2024-12-17 00:47:41 -03:00
button_large_radius='0.375rem',
button_large_padding='6px 12px',
input_radius='0.375rem',
2025-06-08 18:11:27 -07:00
block_radius='0',
2024-12-17 00:47:41 -03:00
)
if (shared.user_data_dir / "notification.mp3").exists():
2023-08-06 21:49:27 -03:00
audio_notification_js = "document.querySelector('#audio_notification audio')?.play();"
else:
audio_notification_js = ""
2023-05-03 21:43:17 -03:00
def list_model_elements():
from modules.loaders import list_model_elements
return list_model_elements()
def list_interface_input_elements():
elements = [
'temperature',
'dynatemp_low',
'dynatemp_high',
'dynatemp_exponent',
'smoothing_factor',
'smoothing_curve',
'min_p',
2025-01-10 18:04:32 -03:00
'top_p',
'top_k',
'typical_p',
2025-01-10 18:04:32 -03:00
'xtc_threshold',
'xtc_probability',
'epsilon_cutoff',
'eta_cutoff',
2025-01-10 18:04:32 -03:00
'tfs',
'top_a',
2025-03-14 16:45:11 -03:00
'top_n_sigma',
'adaptive_target',
'adaptive_decay',
2025-01-10 18:04:32 -03:00
'dry_multiplier',
'dry_allowed_length',
'dry_base',
'repetition_penalty',
'frequency_penalty',
2025-01-10 18:04:32 -03:00
'presence_penalty',
'encoder_repetition_penalty',
'no_repeat_ngram_size',
2025-01-10 18:04:32 -03:00
'repetition_penalty_range',
'penalty_alpha',
2025-01-10 18:04:32 -03:00
'guidance_scale',
'mirostat_mode',
'mirostat_tau',
'mirostat_eta',
2025-01-10 18:04:32 -03:00
'max_new_tokens',
'prompt_lookup_num_tokens',
'max_tokens_second',
'do_sample',
'dynamic_temperature',
'temperature_last',
'auto_max_new_tokens',
'ban_eos_token',
2025-01-10 18:04:32 -03:00
'add_bos_token',
'enable_thinking',
'reasoning_effort',
'skip_special_tokens',
'stream',
'static_cache',
2025-01-10 18:04:32 -03:00
'truncation_length',
'seed',
'sampler_priority',
'custom_stopping_strings',
'custom_token_bans',
'negative_prompt',
'dry_sequence_breakers',
'grammar_string',
'navigate_message_index',
'navigate_direction',
'navigate_message_role',
'edit_message_index',
'edit_message_text',
'edit_message_role',
'branch_index',
2025-05-28 04:27:28 -03:00
'enable_web_search',
'web_search_pages',
]
2023-08-13 01:12:15 -03:00
# Chat elements
elements += [
'history',
2025-01-02 18:46:40 -08:00
'search_chat',
2024-07-21 00:01:42 -03:00
'unique_id',
2025-01-10 18:04:32 -03:00
'textbox',
'start_with',
2026-03-12 01:15:49 -03:00
'selected_tools',
2025-01-10 18:04:32 -03:00
'mode',
'chat_style',
'chat-instruct_command',
'character_menu',
'user_menu',
2023-08-13 01:12:15 -03:00
'name2',
'context',
2025-01-10 18:04:32 -03:00
'greeting',
'name1',
'user_bio',
'custom_system_message',
'instruction_template_str',
'chat_template_str',
2023-08-13 01:12:15 -03:00
]
2023-08-13 01:12:15 -03:00
# Notebook/default elements
elements += [
'textbox-default',
2025-01-10 18:04:32 -03:00
'textbox-notebook',
'prompt_menu-default',
'prompt_menu-notebook',
2025-01-10 18:04:32 -03:00
'output_textbox',
2023-08-13 01:12:15 -03:00
]
# Model elements
elements += list_model_elements()
2023-08-13 01:12:15 -03:00
# Other elements
elements += [
'show_two_notebook_columns',
'paste_to_attachment',
'include_past_attachments',
]
if not shared.args.portable:
# Image generation elements
elements += [
'image_prompt',
'image_neg_prompt',
'image_width',
'image_height',
'image_aspect_ratio',
'image_steps',
'image_cfg_scale',
'image_seed',
'image_batch_size',
'image_batch_count',
'image_llm_variations',
'image_llm_variations_prompt',
'image_model_menu',
'image_dtype',
'image_attn_backend',
'image_compile',
'image_cpu_offload',
'image_quant',
]
2025-12-02 14:55:38 -03:00
2023-04-12 10:27:06 -03:00
return elements
def gather_interface_values(*args):
2024-07-21 22:06:49 -07:00
interface_elements = list_interface_input_elements()
2023-04-12 10:27:06 -03:00
output = {}
2024-07-21 22:06:49 -07:00
for element, value in zip(interface_elements, args):
output[element] = value
if not shared.args.multi_user:
shared.persistent_interface_state = output
# Remove the chat input, as it gets cleared after this function call
shared.persistent_interface_state.pop('textbox')
# Prevent history loss if backend is restarted but UI is not refreshed
if (output['history'] is None or (len(output['history'].get('visible', [])) == 0 and len(output['history'].get('internal', [])) == 0)) and output['unique_id'] is not None:
output['history'] = load_history(output['unique_id'], output['character_menu'], output['mode'])
2023-04-12 10:27:06 -03:00
return output
def apply_interface_values(state, use_persistent=False):
if use_persistent:
state = shared.persistent_interface_state
2024-12-17 00:47:41 -03:00
if 'textbox-default' in state and 'prompt_menu-default' in state:
state.pop('prompt_menu-default')
if 'textbox-notebook' in state and 'prompt_menu-notebook' in state:
state.pop('prompt_menu-notebook')
elements = list_interface_input_elements()
if len(state) == 0:
return [gr.update() for k in elements] # Dummy, do nothing
else:
2023-07-07 09:09:14 -07:00
return [state[k] if k in state else gr.update() for k in elements]
2025-06-11 07:39:49 -07:00
def save_settings(state, preset, extensions_list, show_controls, theme_state, manual_save=False):
output = copy.deepcopy(shared.settings)
exclude = []
for k in state:
if k in shared.settings and k not in exclude:
output[k] = state[k]
if preset:
output['preset'] = preset
output['prompt-notebook'] = state['prompt_menu-default'] if state['show_two_notebook_columns'] else state['prompt_menu-notebook']
if state.get('character_menu'):
output['character'] = state['character_menu']
if state.get('user_menu'):
output['user'] = state['user_menu']
output['seed'] = int(output['seed'])
output['custom_stopping_strings'] = output.get('custom_stopping_strings') or ''
output['custom_token_bans'] = output.get('custom_token_bans') or ''
output['show_controls'] = show_controls
output['dark_theme'] = True if theme_state == 'dark' else False
output.pop('instruction_template_str')
output.pop('truncation_length')
# Handle extensions and extension parameters
2025-06-11 07:39:49 -07:00
if manual_save:
# Save current extensions and their parameter values
2025-06-11 07:39:49 -07:00
output['default_extensions'] = extensions_list
for extension_name in extensions_list:
extension = getattr(extensions, extension_name, None)
if extension:
extension = extension.script
if hasattr(extension, 'params'):
params = getattr(extension, 'params')
for param in params:
_id = f"{extension_name}-{param}"
# Only save if different from default value
if param not in shared.default_settings or params[param] != shared.default_settings[param]:
output[_id] = params[param]
2025-06-11 07:39:49 -07:00
else:
# Preserve existing extensions and extension parameters during autosave
settings_path = shared.user_data_dir / 'settings.yaml'
2025-06-11 07:39:49 -07:00
if settings_path.exists():
try:
with open(settings_path, 'r', encoding='utf-8') as f:
existing_settings = yaml.safe_load(f.read()) or {}
2025-06-11 07:47:25 -07:00
# Preserve default_extensions
2025-06-11 07:39:49 -07:00
if 'default_extensions' in existing_settings:
output['default_extensions'] = existing_settings['default_extensions']
# Preserve extension parameter values
for key, value in existing_settings.items():
if any(key.startswith(f"{ext_name}-") for ext_name in extensions_module.available_extensions):
output[key] = value
2025-06-11 07:39:49 -07:00
except Exception:
pass # If we can't read the file, just don't modify extensions
# Do not save unchanged settings
for key in list(output.keys()):
if key in shared.default_settings and output[key] == shared.default_settings[key]:
output.pop(key)
return yaml.dump(output, sort_keys=False, width=float("inf"), allow_unicode=True)
2025-06-07 22:46:52 -03:00
def store_current_state_and_debounce(interface_state, preset, extensions, show_controls, theme_state):
"""Store current state and trigger debounced save"""
global _auto_save_timer, _last_interface_state, _last_preset, _last_extensions, _last_show_controls, _last_theme_state
if shared.args.multi_user:
return
# Store the current state in global variables
_last_interface_state = interface_state
_last_preset = preset
_last_extensions = extensions
_last_show_controls = show_controls
_last_theme_state = theme_state
# Reset the debounce timer
with _auto_save_lock:
if _auto_save_timer is not None:
_auto_save_timer.cancel()
_auto_save_timer = threading.Timer(1.0, _perform_debounced_save)
2025-06-07 22:46:52 -03:00
_auto_save_timer.start()
def _perform_debounced_save():
"""Actually perform the save using the stored state"""
global _auto_save_timer
try:
if _last_interface_state is not None:
2025-06-11 07:39:49 -07:00
contents = save_settings(_last_interface_state, _last_preset, _last_extensions, _last_show_controls, _last_theme_state, manual_save=False)
settings_path = shared.user_data_dir / 'settings.yaml'
settings_path.parent.mkdir(exist_ok=True)
2025-06-07 22:46:52 -03:00
with open(settings_path, 'w', encoding='utf-8') as f:
f.write(contents)
except Exception as e:
print(f"Auto-save failed: {e}")
finally:
with _auto_save_lock:
_auto_save_timer = None
def setup_auto_save():
"""Attach auto-save to key UI elements"""
if shared.args.multi_user:
return
change_elements = [
# Chat tab (ui_chat.py)
'start_with',
'enable_web_search',
'web_search_pages',
'mode',
'chat_style',
'chat-instruct_command',
'character_menu',
'user_menu',
2025-06-07 22:46:52 -03:00
'name1',
'name2',
'context',
'greeting',
2025-06-07 22:46:52 -03:00
'user_bio',
'custom_system_message',
'chat_template_str',
2026-03-12 01:15:49 -03:00
'selected_tools',
2025-06-07 22:46:52 -03:00
# Parameters tab (ui_parameters.py) - Generation parameters
2025-06-07 22:46:52 -03:00
'preset_menu',
'temperature',
'dynatemp_low',
'dynatemp_high',
'dynatemp_exponent',
'smoothing_factor',
'smoothing_curve',
'min_p',
'top_p',
'top_k',
'typical_p',
'xtc_threshold',
'xtc_probability',
'epsilon_cutoff',
'eta_cutoff',
'tfs',
'top_a',
'top_n_sigma',
'adaptive_target',
'adaptive_decay',
'dry_multiplier',
'dry_allowed_length',
'dry_base',
'repetition_penalty',
'frequency_penalty',
'presence_penalty',
'encoder_repetition_penalty',
'no_repeat_ngram_size',
'repetition_penalty_range',
'penalty_alpha',
'guidance_scale',
'mirostat_mode',
'mirostat_tau',
'mirostat_eta',
2025-06-07 22:46:52 -03:00
'max_new_tokens',
'prompt_lookup_num_tokens',
'max_tokens_second',
'do_sample',
'dynamic_temperature',
'temperature_last',
2025-06-07 22:46:52 -03:00
'auto_max_new_tokens',
'ban_eos_token',
'add_bos_token',
'enable_thinking',
'reasoning_effort',
2025-06-07 22:46:52 -03:00
'skip_special_tokens',
'stream',
'static_cache',
'seed',
'sampler_priority',
2025-06-07 22:46:52 -03:00
'custom_stopping_strings',
'custom_token_bans',
'negative_prompt',
'dry_sequence_breakers',
'grammar_string',
2025-06-07 22:46:52 -03:00
# Default tab (ui_default.py)
'prompt_menu-default',
# Notebook tab (ui_notebook.py)
'prompt_menu-notebook',
# Session tab (ui_session.py)
'show_controls',
'theme_state',
'show_two_notebook_columns',
'paste_to_attachment',
2025-12-02 14:55:38 -03:00
'include_past_attachments',
2025-06-07 22:46:52 -03:00
]
if not shared.args.portable:
# Image generation tab (ui_image_generation.py)
change_elements += [
'image_prompt',
'image_neg_prompt',
'image_width',
'image_height',
'image_aspect_ratio',
'image_steps',
'image_cfg_scale',
'image_seed',
'image_batch_size',
'image_batch_count',
'image_llm_variations',
'image_llm_variations_prompt',
'image_model_menu',
'image_dtype',
'image_attn_backend',
'image_compile',
'image_cpu_offload',
'image_quant',
]
2025-06-07 22:46:52 -03:00
for element_name in change_elements:
if element_name in shared.gradio:
shared.gradio[element_name].change(
gather_interface_values, gradio(shared.input_elements), gradio('interface_state')).then(
store_current_state_and_debounce, gradio('interface_state', 'preset_menu', 'extensions_menu', 'show_controls', 'theme_state'), None, show_progress=False)
2023-09-26 05:44:04 -07:00
def create_refresh_button(refresh_component, refresh_method, refreshed_args, elem_class, interactive=True):
2023-07-25 15:49:04 -07:00
"""
Copied from https://github.com/AUTOMATIC1111/stable-diffusion-webui
"""
def refresh():
refresh_method()
args = refreshed_args() if callable(refreshed_args) else refreshed_args
return gr.update(**(args or {}))
2023-10-10 22:20:49 -03:00
refresh_button = gr.Button(refresh_symbol, elem_classes=elem_class, interactive=interactive)
refresh_button.click(
fn=lambda: {k: tuple(v) if type(k) is list else v for k, v in refresh().items()},
inputs=[],
outputs=[refresh_component]
)
return refresh_button