Config Updates - User settings - Saving Methods
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
|
||||
import json
|
||||
import os # Added for path operations
|
||||
import copy # Added for deepcopy
|
||||
from PySide6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
|
||||
QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QCheckBox,
|
||||
@@ -14,10 +16,10 @@ from PySide6.QtWidgets import QColorDialog, QStyledItemDelegate, QApplication
|
||||
|
||||
# Assuming configuration.py is in the parent directory or accessible
|
||||
try:
|
||||
from configuration import load_base_config, save_base_config
|
||||
from configuration import load_base_config, save_user_config, ConfigurationError
|
||||
except ImportError:
|
||||
# Fallback import for testing or different project structure
|
||||
from ..configuration import load_base_config, save_base_config
|
||||
from ..configuration import load_base_config, save_user_config, ConfigurationError
|
||||
|
||||
|
||||
# --- Custom Delegate for Color Editing ---
|
||||
@@ -86,10 +88,29 @@ class ConfigEditorDialog(QDialog):
|
||||
"""Loads settings from the configuration file."""
|
||||
try:
|
||||
self.settings = load_base_config()
|
||||
print("Configuration loaded successfully.") # Debug print
|
||||
# Store a deep copy of the initial user-configurable settings for granular save.
|
||||
# These are settings from the effective configuration (base + user + defs)
|
||||
# that this dialog manages and are intended for user_settings.json.
|
||||
# Exclude definitions that are stored in separate files or not directly managed here.
|
||||
self.original_user_configurable_settings = {} # Initialize first
|
||||
if self.settings: # Ensure settings were loaded
|
||||
keys_to_copy = [
|
||||
k for k in self.settings
|
||||
if k not in ["ASSET_TYPE_DEFINITIONS", "FILE_TYPE_DEFINITIONS"]
|
||||
]
|
||||
# Create a temporary dictionary with only the keys to be copied
|
||||
temp_original_settings = {
|
||||
k: self.settings[k] for k in keys_to_copy if k in self.settings
|
||||
}
|
||||
self.original_user_configurable_settings = copy.deepcopy(temp_original_settings)
|
||||
print("Original user-configurable settings (relevant parts) deep copied for comparison.") # Debug print
|
||||
else:
|
||||
# If self.settings is None or empty, original_user_configurable_settings remains an empty dict.
|
||||
print("Settings not loaded or empty; original_user_configurable_settings initialized as empty.") # Debug print
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Loading Error", f"Failed to load configuration: {e}")
|
||||
self.settings = {} # Use empty settings on failure
|
||||
self.original_user_configurable_settings = {}
|
||||
# Optionally disable save button or widgets if loading fails
|
||||
self.button_box.button(QDialogButtonBox.Save).setEnabled(False)
|
||||
|
||||
@@ -851,154 +872,162 @@ class ConfigEditorDialog(QDialog):
|
||||
widget.setText(color.name()) # Get color as hex string
|
||||
|
||||
def save_settings(self):
|
||||
"""Reads values from widgets and saves them to the configuration file."""
|
||||
new_settings = {}
|
||||
"""
|
||||
Reads values from widgets, compares them to the original loaded settings,
|
||||
and saves only the changed values to config/user_settings.json, preserving
|
||||
other existing user settings.
|
||||
"""
|
||||
# 1a. Load Current Target File (user_settings.json)
|
||||
# Assuming configuration.py and this file are structured such that
|
||||
# 'config/user_settings.json' is the correct relative path from the workspace root.
|
||||
# TODO: Ideally, get this path from the configuration module if it provides a constant.
|
||||
user_settings_path = os.path.join("config", "user_settings.json")
|
||||
|
||||
# Start with a deep copy of the original settings structure to preserve
|
||||
# sections/keys that might not have dedicated widgets (though ideally all should)
|
||||
import copy
|
||||
new_settings = copy.deepcopy(self.settings)
|
||||
target_file_content = {}
|
||||
if os.path.exists(user_settings_path):
|
||||
try:
|
||||
with open(user_settings_path, 'r') as f:
|
||||
target_file_content = json.load(f)
|
||||
except json.JSONDecodeError:
|
||||
QMessageBox.warning(self, "Warning",
|
||||
f"File {user_settings_path} is corrupted or not valid JSON. "
|
||||
f"It will be overwritten if changes are saved.")
|
||||
target_file_content = {} # Start fresh if corrupted
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Error Loading User Settings",
|
||||
f"Failed to load {user_settings_path}: {e}. "
|
||||
f"Proceeding with empty user settings for this save operation.")
|
||||
target_file_content = {}
|
||||
|
||||
# 1b. Get current settings from UI by populating a full settings dictionary
|
||||
# This `full_ui_state` will mirror the structure of `self.settings` but with current UI values.
|
||||
full_ui_state = copy.deepcopy(self.settings) # Start with the loaded settings structure
|
||||
|
||||
# --- Populate full_ui_state from ALL widgets (adapted from original save_settings logic) ---
|
||||
# This loop iterates through all widgets and updates `full_ui_state` with their current values.
|
||||
# It handles simple widgets and complex ones like tables (though table data for definitions
|
||||
# won't end up in user_settings.json).
|
||||
for widget_config_key, widget_obj in self.widgets.items():
|
||||
# `widget_config_key` is the key used in `self.widgets` (e.g., "OUTPUT_BASE_DIR", "IMAGE_RESOLUTIONS_TABLE")
|
||||
# `keys_path` is for navigating the `full_ui_state` dictionary if `widget_config_key` implies a path.
|
||||
# For most simple widgets, `widget_config_key` is a direct top-level key.
|
||||
keys_path = widget_config_key.split('.')
|
||||
current_level_dict = full_ui_state
|
||||
|
||||
# Navigate to the correct dictionary level if widget_config_key is a path like "general.foo"
|
||||
# For simple keys like "OUTPUT_BASE_DIR", this loop runs once for the key itself.
|
||||
for i, part_of_key in enumerate(keys_path):
|
||||
if i == len(keys_path) - 1: # Last part of the key, time to set the value
|
||||
# Handle simple widgets
|
||||
if isinstance(widget_obj, QLineEdit):
|
||||
if widget_config_key == "RESPECT_VARIANT_MAP_TYPES": # Special list handling
|
||||
current_level_dict[part_of_key] = [item.strip() for item in widget_obj.text().split(',') if item.strip()]
|
||||
else:
|
||||
current_level_dict[part_of_key] = widget_obj.text()
|
||||
elif isinstance(widget_obj, QSpinBox):
|
||||
current_level_dict[part_of_key] = widget_obj.value()
|
||||
elif isinstance(widget_obj, QDoubleSpinBox):
|
||||
current_level_dict[part_of_key] = widget_obj.value()
|
||||
elif isinstance(widget_obj, QCheckBox):
|
||||
current_level_dict[part_of_key] = widget_obj.isChecked()
|
||||
elif isinstance(widget_obj, QComboBox):
|
||||
if widget_config_key == "RESOLUTION_THRESHOLD_FOR_JPG":
|
||||
selected_text = widget_obj.currentText()
|
||||
# Use image_resolutions from the potentially modified full_ui_state
|
||||
image_resolutions_data = full_ui_state.get('IMAGE_RESOLUTIONS', {})
|
||||
if selected_text == "Never": current_level_dict[part_of_key] = 999999
|
||||
elif selected_text == "Always": current_level_dict[part_of_key] = 1
|
||||
elif isinstance(image_resolutions_data, list): # Check if it's the list of [name, val]
|
||||
found_res = next((res[1] for res in image_resolutions_data if res[0] == selected_text), None)
|
||||
if found_res is not None: current_level_dict[part_of_key] = found_res
|
||||
else: current_level_dict[part_of_key] = selected_text # Fallback
|
||||
elif isinstance(image_resolutions_data, dict) and selected_text in image_resolutions_data: # Original format
|
||||
current_level_dict[part_of_key] = image_resolutions_data[selected_text]
|
||||
else: current_level_dict[part_of_key] = selected_text # Fallback
|
||||
else:
|
||||
current_level_dict[part_of_key] = widget_obj.currentText()
|
||||
|
||||
# Handle TableWidgets - only for those relevant to user_settings.json
|
||||
# ASSET_TYPE_DEFINITIONS and FILE_TYPE_DEFINITIONS are handled by their own files.
|
||||
# IMAGE_RESOLUTIONS and MAP_MERGE_RULES might be in user_settings.json.
|
||||
elif widget_config_key == "IMAGE_RESOLUTIONS_TABLE" and isinstance(widget_obj, QTableWidget):
|
||||
table = widget_obj
|
||||
resolutions_list = []
|
||||
for row in range(table.rowCount()):
|
||||
name_item = table.item(row, 0)
|
||||
res_item = table.item(row, 1)
|
||||
if name_item and name_item.text() and res_item and res_item.text():
|
||||
try:
|
||||
# Assuming resolution value might be int or string like "1024" or "1k"
|
||||
# For simplicity, store as string if not easily int.
|
||||
# The config system should handle parsing later.
|
||||
res_val_str = res_item.text()
|
||||
try:
|
||||
res_value = int(res_val_str)
|
||||
except ValueError:
|
||||
res_value = res_val_str # Keep as string if not simple int
|
||||
resolutions_list.append([name_item.text(), res_value])
|
||||
except Exception as e:
|
||||
print(f"Skipping resolution row {row} due to error: {e}")
|
||||
full_ui_state['IMAGE_RESOLUTIONS'] = resolutions_list # Key in settings is IMAGE_RESOLUTIONS
|
||||
|
||||
# MAP_MERGE_RULES are complex; current save logic is a pass-through.
|
||||
# For granular save, if MAP_MERGE_RULES are edited, the full updated list should be in full_ui_state.
|
||||
# The original code had a placeholder for MAP_MERGE_RULES_DATA.
|
||||
# Assuming if MAP_MERGE_RULES are part of user_settings, their full structure from UI
|
||||
# would be placed into full_ui_state['MAP_MERGE_RULES'].
|
||||
# The current implementation of populate_map_merging_tab and display_merge_rule_details
|
||||
# would need to ensure that `full_ui_state['MAP_MERGE_RULES']` is correctly updated
|
||||
# if changes are made via the UI. The original save logic for this was:
|
||||
# `elif key == "MAP_MERGE_RULES_DATA": pass`
|
||||
# This means `full_ui_state['MAP_MERGE_RULES']` would retain its original loaded value unless
|
||||
# other UI interactions (like Add/Remove Rule buttons, if implemented and connected) modify it.
|
||||
# For now, we assume `full_ui_state['MAP_MERGE_RULES']` reflects the intended UI state.
|
||||
|
||||
else: # Navigate deeper
|
||||
if part_of_key not in current_level_dict or not isinstance(current_level_dict[part_of_key], dict):
|
||||
# This case should ideally not happen if widget keys match settings structure
|
||||
# or if the base `full_ui_state` (from `self.settings`) had the correct structure.
|
||||
# If a path implies a dict that doesn't exist, create it.
|
||||
current_level_dict[part_of_key] = {}
|
||||
current_level_dict = current_level_dict[part_of_key]
|
||||
# --- End of populating full_ui_state ---
|
||||
|
||||
# 2. Identify Changes by comparing with self.original_user_configurable_settings
|
||||
changed_settings_count = 0
|
||||
for key_to_check, original_value in self.original_user_configurable_settings.items():
|
||||
# `key_to_check` is a top-level key from the user-configurable settings
|
||||
# (e.g., "OUTPUT_BASE_DIR", "PNG_COMPRESSION_LEVEL", "IMAGE_RESOLUTIONS").
|
||||
current_value_from_ui = full_ui_state.get(key_to_check)
|
||||
|
||||
# Perform comparison. Python's default `!=` works for deep comparison
|
||||
# of basic types, lists, and dicts if order doesn't matter for lists
|
||||
# where it shouldn't (e.g. list of strings) or if dicts are canonical.
|
||||
if current_value_from_ui != original_value:
|
||||
# This setting has changed. Update it in target_file_content.
|
||||
# This replaces the whole value for `key_to_check` in user_settings.
|
||||
target_file_content[key_to_check] = copy.deepcopy(current_value_from_ui)
|
||||
changed_settings_count += 1
|
||||
print(f"Setting '{key_to_check}' changed. Old: {original_value}, New: {current_value_from_ui}")
|
||||
|
||||
|
||||
# Iterate through the stored widgets and update the new_settings dictionary
|
||||
for key, widget in self.widgets.items():
|
||||
# Handle simple widgets
|
||||
if isinstance(widget, (QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QComboBox)):
|
||||
# Split the key to navigate the dictionary structure
|
||||
keys = key.split('.')
|
||||
current_dict = new_settings
|
||||
for i, k in enumerate(keys):
|
||||
if i == len(keys) - 1:
|
||||
# This is the final key, update the value
|
||||
if isinstance(widget, QLineEdit):
|
||||
# Handle simple lists displayed as comma-separated strings
|
||||
if key == "RESPECT_VARIANT_MAP_TYPES":
|
||||
current_dict[k] = [item.strip() for item in widget.text().split(',') if item.strip()]
|
||||
else:
|
||||
current_dict[k] = widget.text()
|
||||
elif isinstance(widget, QSpinBox):
|
||||
current_dict[k] = widget.value()
|
||||
elif isinstance(widget, QDoubleSpinBox):
|
||||
current_dict[k] = widget.value()
|
||||
elif isinstance(widget, QCheckBox):
|
||||
current_dict[k] = widget.isChecked()
|
||||
elif isinstance(widget, QComboBox):
|
||||
# Special handling for RESOLUTION_THRESHOLD_FOR_JPG
|
||||
if key == "RESOLUTION_THRESHOLD_FOR_JPG": # Use 'key' from the loop, not 'full_key'
|
||||
selected_text = widget.currentText()
|
||||
image_resolutions = new_settings.get('IMAGE_RESOLUTIONS', {})
|
||||
if selected_text == "Never":
|
||||
# Use a very large number so the comparison target_dim_px >= threshold is always false
|
||||
current_dict[k] = 999999
|
||||
elif selected_text == "Always":
|
||||
current_dict[k] = 1 # Assuming 1 means always apply (any positive dimension >= 1)
|
||||
elif selected_text in image_resolutions:
|
||||
current_dict[k] = image_resolutions[selected_text]
|
||||
else:
|
||||
# Fallback or error handling if text is unexpected
|
||||
print(f"Warning: Unexpected value '{selected_text}' for RESOLUTION_THRESHOLD_FOR_JPG. Saving as text.")
|
||||
current_dict[k] = selected_text # Save original text as fallback
|
||||
else:
|
||||
# Default behavior for other combo boxes
|
||||
current_dict[k] = widget.currentText()
|
||||
else:
|
||||
# Navigate to the next level
|
||||
# Use full_key for error message consistency
|
||||
if k not in current_dict or not isinstance(current_dict[k], dict):
|
||||
# This should not happen if create_tabs is correct, but handle defensively
|
||||
print(f"Warning: Structure mismatch for key part '{k}' in '{key}'")
|
||||
break # Stop processing this key
|
||||
current_dict = current_dict[k]
|
||||
# Handle TableWidgets (for definitions and image resolutions)
|
||||
elif key == "ASSET_TYPE_DEFINITIONS_TABLE":
|
||||
table = widget
|
||||
asset_defs = {}
|
||||
for row in range(table.rowCount()):
|
||||
try:
|
||||
type_name_item = table.item(row, 0)
|
||||
desc_item = table.item(row, 1)
|
||||
color_item = table.item(row, 2) # Delegate stores color in EditRole
|
||||
examples_item = table.item(row, 3)
|
||||
|
||||
if not type_name_item or not type_name_item.text():
|
||||
print(f"Warning: Skipping row {row} in {key} due to missing type name.")
|
||||
continue
|
||||
|
||||
type_name = type_name_item.text()
|
||||
description = desc_item.text() if desc_item else ""
|
||||
# Get color from EditRole data set by the delegate
|
||||
color_str = color_item.data(Qt.EditRole) if color_item else "#ffffff"
|
||||
examples_str = examples_item.text() if examples_item else ""
|
||||
examples_list = [ex.strip() for ex in examples_str.split(',') if ex.strip()]
|
||||
|
||||
asset_defs[type_name] = {
|
||||
"description": description,
|
||||
"color": color_str,
|
||||
"examples": examples_list
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error processing row {row} in {key}: {e}")
|
||||
new_settings['ASSET_TYPE_DEFINITIONS'] = asset_defs # Overwrite with new data
|
||||
|
||||
elif key == "FILE_TYPE_DEFINITIONS_TABLE":
|
||||
table = widget
|
||||
file_defs = {}
|
||||
for row in range(table.rowCount()):
|
||||
try:
|
||||
type_id_item = table.item(row, 0)
|
||||
desc_item = table.item(row, 1)
|
||||
color_item = table.item(row, 2) # Delegate stores color in EditRole
|
||||
examples_item = table.item(row, 3)
|
||||
std_type_item = table.item(row, 4)
|
||||
bit_depth_item = table.item(row, 5)
|
||||
|
||||
if not type_id_item or not type_id_item.text():
|
||||
print(f"Warning: Skipping row {row} in {key} due to missing type ID.")
|
||||
continue
|
||||
|
||||
type_id = type_id_item.text()
|
||||
description = desc_item.text() if desc_item else ""
|
||||
# Get color from EditRole data set by the delegate
|
||||
color_str = color_item.data(Qt.EditRole) if color_item else "#ffffff"
|
||||
examples_str = examples_item.text() if examples_item else ""
|
||||
examples_list = [ex.strip() for ex in examples_str.split(',') if ex.strip()]
|
||||
standard_type = std_type_item.text() if std_type_item else ""
|
||||
bit_depth_rule = bit_depth_item.text() if bit_depth_item else "respect" # Default?
|
||||
|
||||
file_defs[type_id] = {
|
||||
"description": description,
|
||||
"color": color_str,
|
||||
"examples": examples_list,
|
||||
"standard_type": standard_type,
|
||||
"bit_depth_rule": bit_depth_rule
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Error processing row {row} in {key}: {e}")
|
||||
new_settings['FILE_TYPE_DEFINITIONS'] = file_defs # Overwrite with new data
|
||||
|
||||
elif key in ["IMAGE_RESOLUTIONS_TABLE", "MAP_BIT_DEPTH_RULES_TABLE"]:
|
||||
# Still not implemented for these tables
|
||||
print(f"Warning: Saving for table widget '{key}' is not fully implemented.")
|
||||
pass # Defer saving complex table data
|
||||
|
||||
# Handle Map Merge Rules (more complex)
|
||||
# Note: Changes made in the details form are NOT saved with this implementation
|
||||
# due to deferred complexity in updating the list item's data and reconstructing the list.
|
||||
elif key == "MAP_MERGE_RULES_DATA":
|
||||
# The original data is stored, but changes in the details form are not reflected.
|
||||
# A full implementation would require updating the rule data in the list item
|
||||
# when the details form is edited and then reconstructing the list.
|
||||
print(f"Warning: Saving for Map Merge Rules details is not fully implemented.")
|
||||
pass # Defer saving changes from the details form
|
||||
|
||||
|
||||
# Save the new settings
|
||||
try:
|
||||
save_base_config(new_settings)
|
||||
QMessageBox.information(self, "Settings Saved", "Configuration saved successfully.\nRestart the application to apply changes.")
|
||||
self.accept() # Close the dialog
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Saving Error", f"Failed to save configuration: {e}")
|
||||
# 3. Save Updated Content to user_settings.json
|
||||
if changed_settings_count > 0 or not os.path.exists(user_settings_path):
|
||||
# Save if there are changes or if the file didn't exist (to create it with defaults if any were set)
|
||||
try:
|
||||
save_user_config(target_file_content) # save_user_config is imported
|
||||
QMessageBox.information(self, "Settings Saved",
|
||||
f"User settings saved successfully to {user_settings_path}.\n"
|
||||
f"{changed_settings_count} setting(s) updated. "
|
||||
"Some changes may require an application restart.")
|
||||
self.accept() # Close the dialog
|
||||
except ConfigurationError as e:
|
||||
QMessageBox.critical(self, "Saving Error", f"Failed to save user configuration: {e}")
|
||||
except Exception as e:
|
||||
QMessageBox.critical(self, "Saving Error", f"An unexpected error occurred while saving: {e}")
|
||||
else:
|
||||
QMessageBox.information(self, "No Changes", "No changes were made to user-configurable settings.")
|
||||
self.accept() # Close the dialog, or self.reject() if no changes means cancel
|
||||
|
||||
def populate_widgets_from_settings(self):
|
||||
"""Populates the created widgets with loaded settings."""
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# gui/llm_editor_widget.py
|
||||
import json
|
||||
import logging
|
||||
import copy # Added for deepcopy
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget, QVBoxLayout, QTabWidget, QPlainTextEdit, QGroupBox,
|
||||
QHBoxLayout, QPushButton, QFormLayout, QLineEdit, QDoubleSpinBox,
|
||||
@@ -24,6 +25,7 @@ class LLMEditorWidget(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self._unsaved_changes = False
|
||||
self.original_llm_settings = {} # Initialize original_llm_settings
|
||||
self._init_ui()
|
||||
self._connect_signals()
|
||||
self.save_button.setEnabled(False) # Initially disabled
|
||||
@@ -131,6 +133,7 @@ class LLMEditorWidget(QWidget):
|
||||
try:
|
||||
with open(LLM_CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
settings = json.load(f)
|
||||
self.original_llm_settings = copy.deepcopy(settings) # Store a deep copy
|
||||
|
||||
# Populate Prompt Settings
|
||||
self.prompt_editor.setPlainText(settings.get("llm_predictor_prompt", ""))
|
||||
@@ -159,9 +162,9 @@ class LLMEditorWidget(QWidget):
|
||||
logger.info("LLM settings loaded successfully.")
|
||||
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"LLM settings file not found: {LLM_CONFIG_PATH}. Using defaults and disabling editor.")
|
||||
logger.warning(f"LLM settings file not found: {LLM_CONFIG_PATH}. Using defaults.")
|
||||
QMessageBox.warning(self, "Load Error",
|
||||
f"LLM settings file not found:\n{LLM_CONFIG_PATH}\n\nPlease ensure the file exists. Using default values.")
|
||||
f"LLM settings file not found:\n{LLM_CONFIG_PATH}\n\nNew settings will be created if you save.")
|
||||
# Reset to defaults (optional, or leave fields empty)
|
||||
self.prompt_editor.clear()
|
||||
self.endpoint_url_edit.clear()
|
||||
@@ -169,19 +172,21 @@ class LLMEditorWidget(QWidget):
|
||||
self.model_name_edit.clear()
|
||||
self.temperature_spinbox.setValue(0.7)
|
||||
self.timeout_spinbox.setValue(120)
|
||||
# self.setEnabled(False) # Disabling might be too harsh if user wants to create settings
|
||||
self.original_llm_settings = {} # Start with empty original settings if file not found
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error decoding JSON from {LLM_CONFIG_PATH}: {e}")
|
||||
QMessageBox.critical(self, "Load Error",
|
||||
f"Failed to parse LLM settings file:\n{LLM_CONFIG_PATH}\n\nError: {e}\n\nPlease check the file for syntax errors. Editor will be disabled.")
|
||||
self.setEnabled(False) # Disable editor on critical load error
|
||||
self.original_llm_settings = {} # Reset original settings on JSON error
|
||||
|
||||
except Exception as e: # Catch other potential errors during loading/populating
|
||||
logger.error(f"An unexpected error occurred loading LLM settings: {e}", exc_info=True)
|
||||
QMessageBox.critical(self, "Load Error",
|
||||
f"An unexpected error occurred while loading settings:\n{e}\n\nEditor will be disabled.")
|
||||
self.setEnabled(False)
|
||||
self.original_llm_settings = {} # Reset original settings on other errors
|
||||
|
||||
|
||||
# Reset unsaved changes flag and disable save button after loading
|
||||
@@ -201,26 +206,38 @@ class LLMEditorWidget(QWidget):
|
||||
"""Gather data from UI, save to JSON file, and handle errors."""
|
||||
logger.info("Attempting to save LLM settings...")
|
||||
|
||||
settings_dict = {}
|
||||
# 1.a. Load Current Target File
|
||||
target_file_content = {}
|
||||
try:
|
||||
with open(LLM_CONFIG_PATH, 'r', encoding='utf-8') as f:
|
||||
target_file_content = json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.info(f"{LLM_CONFIG_PATH} not found. Will create a new one.")
|
||||
target_file_content = {} # Start with an empty dict if file doesn't exist
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Error decoding existing {LLM_CONFIG_PATH}: {e}. Starting with an empty config for save.")
|
||||
QMessageBox.warning(self, "Warning",
|
||||
f"Could not parse existing LLM settings file ({LLM_CONFIG_PATH}).\n"
|
||||
f"Any pre-existing settings in that file might be overwritten if you save now.\nError: {e}")
|
||||
target_file_content = {} # Start fresh if current file is corrupt
|
||||
|
||||
# 1.b. Gather current UI settings into current_llm_settings
|
||||
current_llm_settings = {}
|
||||
parsed_examples = []
|
||||
has_errors = False
|
||||
has_errors = False # For example parsing
|
||||
|
||||
# Gather API Settings
|
||||
settings_dict["llm_endpoint_url"] = self.endpoint_url_edit.text().strip()
|
||||
settings_dict["llm_api_key"] = self.api_key_edit.text() # Keep as is, don't strip
|
||||
settings_dict["llm_model_name"] = self.model_name_edit.text().strip()
|
||||
settings_dict["llm_temperature"] = self.temperature_spinbox.value()
|
||||
settings_dict["llm_request_timeout"] = self.timeout_spinbox.value()
|
||||
current_llm_settings["llm_endpoint_url"] = self.endpoint_url_edit.text().strip()
|
||||
current_llm_settings["llm_api_key"] = self.api_key_edit.text() # Keep as is
|
||||
current_llm_settings["llm_model_name"] = self.model_name_edit.text().strip()
|
||||
current_llm_settings["llm_temperature"] = self.temperature_spinbox.value()
|
||||
current_llm_settings["llm_request_timeout"] = self.timeout_spinbox.value()
|
||||
current_llm_settings["llm_predictor_prompt"] = self.prompt_editor.toPlainText().strip()
|
||||
|
||||
# Gather Prompt Settings
|
||||
settings_dict["llm_predictor_prompt"] = self.prompt_editor.toPlainText().strip()
|
||||
|
||||
# Gather and Parse Examples
|
||||
for i in range(self.examples_tab_widget.count()):
|
||||
example_editor = self.examples_tab_widget.widget(i)
|
||||
if isinstance(example_editor, QTextEdit):
|
||||
example_text = example_editor.toPlainText().strip()
|
||||
if not example_text: # Skip empty examples silently
|
||||
if not example_text:
|
||||
continue
|
||||
try:
|
||||
parsed_example = json.loads(example_text)
|
||||
@@ -231,40 +248,58 @@ class LLMEditorWidget(QWidget):
|
||||
logger.warning(f"Invalid JSON in '{tab_name}': {e}. Skipping example.")
|
||||
QMessageBox.warning(self, "Invalid Example",
|
||||
f"The content in '{tab_name}' is not valid JSON and will not be saved.\n\nError: {e}\n\nPlease correct it or remove the tab.")
|
||||
# Optionally switch to the tab with the error:
|
||||
# self.examples_tab_widget.setCurrentIndex(i)
|
||||
else:
|
||||
logger.warning(f"Widget at index {i} in examples tab is not a QTextEdit. Skipping.")
|
||||
|
||||
logger.warning(f"Widget at index {i} in examples tab is not a QTextEdit. Skipping.")
|
||||
|
||||
if has_errors:
|
||||
logger.warning("LLM settings not saved due to invalid JSON in examples.")
|
||||
# Keep save button enabled if there were errors, allowing user to fix and retry
|
||||
# self.save_button.setEnabled(True)
|
||||
# self._unsaved_changes = True
|
||||
return # Stop saving process
|
||||
return
|
||||
|
||||
settings_dict["llm_predictor_examples"] = parsed_examples
|
||||
current_llm_settings["llm_predictor_examples"] = parsed_examples
|
||||
|
||||
# Save the dictionary to file
|
||||
# 1.c. Identify Changes and Update Target File Content
|
||||
changed_settings_count = 0
|
||||
for key, current_value in current_llm_settings.items():
|
||||
original_value = self.original_llm_settings.get(key)
|
||||
|
||||
# Special handling for lists (e.g., examples) - direct comparison works
|
||||
# For other types, direct comparison also works.
|
||||
# This includes new keys present in current_llm_settings but not in original_llm_settings
|
||||
if key not in self.original_llm_settings or current_value != original_value:
|
||||
target_file_content[key] = current_value
|
||||
logger.debug(f"Setting '{key}' changed or added. Old: '{original_value}', New: '{current_value}'")
|
||||
changed_settings_count +=1
|
||||
|
||||
if changed_settings_count == 0 and self._unsaved_changes:
|
||||
logger.info("Save called, but no actual changes detected compared to original loaded settings.")
|
||||
# If _unsaved_changes was true, it means UI interaction happened,
|
||||
# but values might have been reverted to original.
|
||||
# We still proceed to save target_file_content as it might contain
|
||||
# values from a file that was modified externally since last load.
|
||||
# Or, if the file didn't exist, it will now be created with current UI values.
|
||||
|
||||
# 1.d. Save Updated Content
|
||||
try:
|
||||
save_llm_config(settings_dict)
|
||||
save_llm_config(target_file_content) # Save the potentially modified target_file_content
|
||||
QMessageBox.information(self, "Save Successful", f"LLM settings saved to:\n{LLM_CONFIG_PATH}")
|
||||
|
||||
# Update original_llm_settings to reflect the newly saved state
|
||||
self.original_llm_settings = copy.deepcopy(target_file_content)
|
||||
|
||||
self.save_button.setEnabled(False)
|
||||
self._unsaved_changes = False
|
||||
self.settings_saved.emit() # Notify MainWindow or others
|
||||
self.settings_saved.emit()
|
||||
logger.info("LLM settings saved successfully.")
|
||||
|
||||
except ConfigurationError as e:
|
||||
logger.error(f"Failed to save LLM settings: {e}")
|
||||
QMessageBox.critical(self, "Save Error", f"Could not save LLM settings.\n\nError: {e}")
|
||||
# Keep save button enabled as save failed
|
||||
self.save_button.setEnabled(True)
|
||||
self.save_button.setEnabled(True) # Keep save enabled
|
||||
self._unsaved_changes = True
|
||||
except Exception as e: # Catch unexpected errors during save
|
||||
except Exception as e:
|
||||
logger.error(f"An unexpected error occurred during LLM settings save: {e}", exc_info=True)
|
||||
QMessageBox.critical(self, "Save Error", f"An unexpected error occurred while saving settings:\n{e}")
|
||||
self.save_button.setEnabled(True)
|
||||
self.save_button.setEnabled(True) # Keep save enabled
|
||||
self._unsaved_changes = True
|
||||
|
||||
# --- Example Management Slots ---
|
||||
|
||||
Reference in New Issue
Block a user