""" Configuration Tab - Handles environment variables and global settings """ import os from pathlib import Path from PyQt5.QtWidgets import ( QWidget, QVBoxLayout, QHBoxLayout, QFormLayout, QLineEdit, QSpinBox, QDoubleSpinBox, QComboBox, QPushButton, QGroupBox, QLabel, QFileDialog, QMessageBox, QScrollArea, QTextEdit, QCheckBox, QApplication ) from PyQt5.QtCore import Qt, pyqtSignal from dotenv import load_dotenv, set_key class ConfigTab(QWidget): """Configuration tab for managing environment variables and global settings.""" config_changed = pyqtSignal() def __init__(self): super().__init__() self.env_file_path = Path(".env") self.init_ui() self.load_from_env() def init_ui(self): """Initialize the user interface.""" main_layout = QVBoxLayout() # Create scroll area scroll_area = QScrollArea() scroll_content = QWidget() scroll_layout = QVBoxLayout() # API Configuration Group api_group = self.create_api_group() scroll_layout.addWidget(api_group) # Translation Settings Group translation_group = self.create_translation_group() scroll_layout.addWidget(translation_group) # Performance Settings Group performance_group = self.create_performance_group() scroll_layout.addWidget(performance_group) # Text Formatting Group formatting_group = self.create_formatting_group() scroll_layout.addWidget(formatting_group) # Custom API Settings Group custom_api_group = self.create_custom_api_group() scroll_layout.addWidget(custom_api_group) # UI Settings Group ui_group = self.create_ui_settings_group() scroll_layout.addWidget(ui_group) # Buttons button_layout = QHBoxLayout() self.save_button = QPushButton("Save Configuration") self.save_button.clicked.connect(self.save_to_env) self.load_button = QPushButton("Load from File") self.load_button.clicked.connect(self.load_from_file_dialog) self.reset_button = QPushButton("Reset to Defaults") self.reset_button.clicked.connect(self.reset_to_defaults) button_layout.addWidget(self.save_button) button_layout.addWidget(self.load_button) button_layout.addWidget(self.reset_button) button_layout.addStretch() scroll_layout.addLayout(button_layout) scroll_content.setLayout(scroll_layout) scroll_area.setWidget(scroll_content) scroll_area.setWidgetResizable(True) main_layout.addWidget(scroll_area) self.setLayout(main_layout) def create_api_group(self): """Create API configuration group.""" group = QGroupBox("API Configuration") layout = QFormLayout() # API URL self.api_url_edit = QLineEdit() self.api_url_edit.setPlaceholderText("Leave blank for OpenAI API") layout.addRow("API URL:", self.api_url_edit) # API Key self.api_key_edit = QLineEdit() self.api_key_edit.setEchoMode(QLineEdit.Password) self.api_key_edit.setPlaceholderText("Enter your API key") layout.addRow("API Key:", self.api_key_edit) # Organization self.organization_edit = QLineEdit() self.organization_edit.setPlaceholderText("Organization key") layout.addRow("Organization:", self.organization_edit) # Model self.model_combo = QComboBox() self.model_combo.setEditable(True) self.model_combo.addItems([ "gpt-3.5-turbo", "gpt-3.5-turbo-1106", "gpt-4", "gpt-4-1106-preview", "gpt-4-turbo", "claude-3-sonnet-20240229", "claude-3-opus-20240229" ]) layout.addRow("Model:", self.model_combo) group.setLayout(layout) return group def create_translation_group(self): """Create translation settings group.""" group = QGroupBox("Translation Settings") layout = QFormLayout() # Language self.language_combo = QComboBox() self.language_combo.addItems([ "English", "Spanish", "French", "German", "Italian", "Portuguese", "Russian", "Chinese", "Korean", "Japanese" ]) layout.addRow("Target Language:", self.language_combo) # Timeout self.timeout_spin = QSpinBox() self.timeout_spin.setRange(30, 300) self.timeout_spin.setValue(120) self.timeout_spin.setSuffix(" seconds") layout.addRow("Timeout:", self.timeout_spin) group.setLayout(layout) return group def create_performance_group(self): """Create performance settings group.""" group = QGroupBox("Performance Settings") layout = QFormLayout() # File Threads self.file_threads_spin = QSpinBox() self.file_threads_spin.setRange(1, 10) self.file_threads_spin.setValue(1) layout.addRow("File Threads:", self.file_threads_spin) # Threads per File self.threads_spin = QSpinBox() self.threads_spin.setRange(1, 20) self.threads_spin.setValue(1) layout.addRow("Threads per File:", self.threads_spin) # Batch Size self.batch_size_spin = QSpinBox() self.batch_size_spin.setRange(1, 100) self.batch_size_spin.setValue(10) layout.addRow("Batch Size:", self.batch_size_spin) # Frequency Penalty self.frequency_penalty_spin = QDoubleSpinBox() self.frequency_penalty_spin.setRange(0.0, 2.0) self.frequency_penalty_spin.setSingleStep(0.1) self.frequency_penalty_spin.setValue(0.2) layout.addRow("Frequency Penalty:", self.frequency_penalty_spin) group.setLayout(layout) return group def create_formatting_group(self): """Create text formatting settings group.""" group = QGroupBox("Text Formatting") layout = QFormLayout() # Dialogue Width self.width_spin = QSpinBox() self.width_spin.setRange(20, 200) self.width_spin.setValue(60) self.width_spin.setSuffix(" characters") layout.addRow("Dialogue Width:", self.width_spin) # List Width self.list_width_spin = QSpinBox() self.list_width_spin.setRange(20, 200) self.list_width_spin.setValue(100) self.list_width_spin.setSuffix(" characters") layout.addRow("List Width:", self.list_width_spin) # Note Width self.note_width_spin = QSpinBox() self.note_width_spin.setRange(20, 200) self.note_width_spin.setValue(75) self.note_width_spin.setSuffix(" characters") layout.addRow("Note Width:", self.note_width_spin) group.setLayout(layout) return group def create_custom_api_group(self): """Create custom API settings group.""" group = QGroupBox("Custom API Pricing") layout = QFormLayout() # Input Cost self.input_cost_spin = QDoubleSpinBox() self.input_cost_spin.setRange(0.0, 1.0) self.input_cost_spin.setDecimals(4) self.input_cost_spin.setSingleStep(0.0001) self.input_cost_spin.setValue(0.002) self.input_cost_spin.setSuffix(" per 1K tokens") layout.addRow("Input Cost:", self.input_cost_spin) # Output Cost self.output_cost_spin = QDoubleSpinBox() self.output_cost_spin.setRange(0.0, 1.0) self.output_cost_spin.setDecimals(4) self.output_cost_spin.setSingleStep(0.0001) self.output_cost_spin.setValue(0.002) self.output_cost_spin.setSuffix(" per 1K tokens") layout.addRow("Output Cost:", self.output_cost_spin) group.setLayout(layout) return group def create_ui_settings_group(self): """Create UI settings group.""" group = QGroupBox("User Interface Settings") layout = QFormLayout() # Font Scale self.font_scale_spin = QDoubleSpinBox() self.font_scale_spin.setRange(0.5, 3.0) self.font_scale_spin.setSingleStep(0.1) self.font_scale_spin.setValue(1.0) self.font_scale_spin.setDecimals(1) self.font_scale_spin.setSuffix("x") self.font_scale_spin.setToolTip("Scale factor for all fonts in the GUI (1.0 = normal, 1.5 = 150% larger)") self.font_scale_spin.valueChanged.connect(self.on_font_scale_changed) # Font scale presets font_layout = QHBoxLayout() font_layout.addWidget(self.font_scale_spin) # Quick preset buttons small_btn = QPushButton("Small") small_btn.clicked.connect(lambda: self.font_scale_spin.setValue(0.8)) small_btn.setMaximumWidth(60) normal_btn = QPushButton("Normal") normal_btn.clicked.connect(lambda: self.font_scale_spin.setValue(1.0)) normal_btn.setMaximumWidth(60) large_btn = QPushButton("Large") large_btn.clicked.connect(lambda: self.font_scale_spin.setValue(1.5)) large_btn.setMaximumWidth(60) xlarge_btn = QPushButton("X-Large") xlarge_btn.clicked.connect(lambda: self.font_scale_spin.setValue(2.0)) xlarge_btn.setMaximumWidth(60) font_layout.addWidget(small_btn) font_layout.addWidget(normal_btn) font_layout.addWidget(large_btn) font_layout.addWidget(xlarge_btn) layout.addRow("Font Scale:", font_layout) # DPI Awareness self.dpi_aware_cb = QCheckBox("High DPI Scaling") self.dpi_aware_cb.setToolTip("Enable automatic scaling for high DPI displays") self.dpi_aware_cb.setChecked(True) layout.addRow("", self.dpi_aware_cb) # Theme selection self.theme_combo = QComboBox() self.theme_combo.addItems(["Dark", "Light", "System"]) self.theme_combo.setCurrentText("Dark") layout.addRow("Theme:", self.theme_combo) # Show font tip self.show_font_tip_cb = QCheckBox("Show font size tip on startup") self.show_font_tip_cb.setToolTip("Display helpful font sizing information when the GUI starts") self.show_font_tip_cb.setChecked(True) layout.addRow("", self.show_font_tip_cb) group.setLayout(layout) return group def on_font_scale_changed(self, value): """Handle font scale change.""" self.config_changed.emit() # Apply font scaling immediately self.apply_font_scaling(value) def apply_font_scaling(self, scale_factor): """Apply font scaling to the entire application.""" app = QApplication.instance() if app: # Get the default font font = app.font() base_size = 9 # Base font size new_size = int(base_size * scale_factor) font.setPointSize(new_size) app.setFont(font) # Emit signal to notify other components self.config_changed.emit() def load_from_env(self): """Load configuration from .env file.""" if self.env_file_path.exists(): load_dotenv(self.env_file_path) # Load API settings self.api_url_edit.setText(os.getenv("api", "")) self.api_key_edit.setText(os.getenv("key", "")) self.organization_edit.setText(os.getenv("organization", "")) self.model_combo.setCurrentText(os.getenv("model", "gpt-4")) # Load translation settings self.language_combo.setCurrentText(os.getenv("language", "English")) self.timeout_spin.setValue(int(os.getenv("timeout", "120"))) # Load performance settings self.file_threads_spin.setValue(int(os.getenv("fileThreads", "1"))) self.threads_spin.setValue(int(os.getenv("threads", "1"))) self.batch_size_spin.setValue(int(os.getenv("batchsize", "10"))) self.frequency_penalty_spin.setValue(float(os.getenv("frequency_penalty", "0.2"))) # Load formatting settings self.width_spin.setValue(int(os.getenv("width", "60"))) self.list_width_spin.setValue(int(os.getenv("listWidth", "100"))) self.note_width_spin.setValue(int(os.getenv("noteWidth", "80"))) # Load custom API settings self.input_cost_spin.setValue(float(os.getenv("input_cost", "0.002"))) self.output_cost_spin.setValue(float(os.getenv("output_cost", "0.002"))) # Load UI settings self.font_scale_spin.setValue(float(os.getenv("font_scale", "1.0"))) self.dpi_aware_cb.setChecked(os.getenv("dpi_aware", "true").lower() == "true") self.theme_combo.setCurrentText(os.getenv("theme", "Dark")) self.show_font_tip_cb.setChecked(os.getenv("show_font_tip", "true").lower() == "true") # Apply font scaling self.apply_font_scaling(self.font_scale_spin.value()) def save_to_env(self): """Save configuration to .env file.""" try: # Ensure .env file exists if not self.env_file_path.exists(): self.env_file_path.touch() # Save API settings set_key(self.env_file_path, "api", self.api_url_edit.text()) set_key(self.env_file_path, "key", self.api_key_edit.text()) set_key(self.env_file_path, "organization", self.organization_edit.text()) set_key(self.env_file_path, "model", self.model_combo.currentText()) # Save translation settings set_key(self.env_file_path, "language", self.language_combo.currentText()) set_key(self.env_file_path, "timeout", str(self.timeout_spin.value())) # Save performance settings set_key(self.env_file_path, "fileThreads", str(self.file_threads_spin.value())) set_key(self.env_file_path, "threads", str(self.threads_spin.value())) set_key(self.env_file_path, "batchsize", str(self.batch_size_spin.value())) set_key(self.env_file_path, "frequency_penalty", str(self.frequency_penalty_spin.value())) # Save formatting settings set_key(self.env_file_path, "width", str(self.width_spin.value())) set_key(self.env_file_path, "listWidth", str(self.list_width_spin.value())) set_key(self.env_file_path, "noteWidth", str(self.note_width_spin.value())) # Save custom API settings set_key(self.env_file_path, "input_cost", str(self.input_cost_spin.value())) set_key(self.env_file_path, "output_cost", str(self.output_cost_spin.value())) # Save UI settings set_key(self.env_file_path, "font_scale", str(self.font_scale_spin.value())) set_key(self.env_file_path, "dpi_aware", str(self.dpi_aware_cb.isChecked()).lower()) set_key(self.env_file_path, "theme", self.theme_combo.currentText()) set_key(self.env_file_path, "show_font_tip", str(self.show_font_tip_cb.isChecked()).lower()) QMessageBox.information(self, "Success", "Configuration saved successfully!") self.config_changed.emit() except Exception as e: QMessageBox.critical(self, "Error", f"Failed to save configuration:\n{str(e)}") def load_from_file_dialog(self): """Load configuration from a file via dialog.""" file_path, _ = QFileDialog.getOpenFileName( self, "Load Configuration", "", "Environment Files (*.env);;All Files (*)" ) if file_path: self.load_from_file(file_path) def load_from_file(self, file_path): """Load configuration from a specific file.""" try: load_dotenv(file_path, override=True) self.load_from_env() QMessageBox.information(self, "Success", f"Configuration loaded from {Path(file_path).name}") except Exception as e: QMessageBox.critical(self, "Error", f"Failed to load configuration:\n{str(e)}") def reset_to_defaults(self): """Reset all settings to default values.""" # API settings self.api_url_edit.clear() self.api_key_edit.clear() self.organization_edit.clear() self.model_combo.setCurrentText("gpt-4.1") # Translation settings self.language_combo.setCurrentText("English") self.timeout_spin.setValue(120) # Performance settings self.file_threads_spin.setValue(1) self.threads_spin.setValue(1) self.batch_size_spin.setValue(30) self.frequency_penalty_spin.setValue(0.2) # Formatting settings self.width_spin.setValue(60) self.list_width_spin.setValue(80) self.note_width_spin.setValue(85) # Custom API settings self.input_cost_spin.setValue(0.002) self.output_cost_spin.setValue(0.002) # UI settings self.font_scale_spin.setValue(1.0) self.dpi_aware_cb.setChecked(True) self.theme_combo.setCurrentText("Dark") self.show_font_tip_cb.setChecked(True) # Apply font scaling self.apply_font_scaling(1.0) def get_config(self): """Get current configuration as dictionary.""" return { "api": self.api_url_edit.text(), "key": self.api_key_edit.text(), "organization": self.organization_edit.text(), "model": self.model_combo.currentText(), "language": self.language_combo.currentText(), "timeout": self.timeout_spin.value(), "fileThreads": self.file_threads_spin.value(), "threads": self.threads_spin.value(), "batchsize": self.batch_size_spin.value(), "frequency_penalty": self.frequency_penalty_spin.value(), "width": self.width_spin.value(), "listWidth": self.list_width_spin.value(), "noteWidth": self.note_width_spin.value(), "input_cost": self.input_cost_spin.value(), "output_cost": self.output_cost_spin.value(), "font_scale": self.font_scale_spin.value(), "dpi_aware": self.dpi_aware_cb.isChecked(), "theme": self.theme_combo.currentText(), "show_font_tip": self.show_font_tip_cb.isChecked() } def validate(self): """Validate the current configuration.""" errors = [] # Check required fields if not self.api_key_edit.text().strip(): errors.append("API Key is required") if not self.model_combo.currentText().strip(): errors.append("Model is required") # Check numeric ranges if self.timeout_spin.value() < 30: errors.append("Timeout should be at least 30 seconds") if self.threads_spin.value() > 10 and "gpt-4" in self.model_combo.currentText().lower(): errors.append("Too many threads for GPT-4 - recommended: 1-2") if errors: QMessageBox.warning(self, "Validation Errors", "\n".join(errors)) return False return True