# individual_endpoint_dialog.py """ Individual Endpoint Configuration Dialog for Glossarion - Allows enabling/disabling per-key custom endpoint (e.g., Azure, Ollama/local OpenAI-compatible) - Persists changes to the in-memory key object and refreshes the parent list """ import os from PySide6.QtWidgets import ( QDialog, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QLineEdit, QCheckBox, QComboBox, QGroupBox, QGridLayout, QFrame, QScrollArea, QWidget, QMessageBox ) from PySide6.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve from PySide6.QtGui import QFont, QPixmap, QTransform from spinning import create_icon_label, animate_icon from typing import Callable try: # For type hints only; not required at runtime from multi_api_key_manager import APIKeyEntry # noqa: F401 except Exception: pass class IndividualEndpointDialog(QDialog): def __init__(self, parent, translator_gui, key, refresh_callback: Callable[[], None], status_callback: Callable[[str], None]): super().__init__(parent) self.translator_gui = translator_gui self.key = key self.refresh_callback = refresh_callback self.status_callback = status_callback self._build() def _create_styled_checkbox(self, text): """Create a checkbox with proper checkmark using text overlay""" checkbox = QCheckBox(text) # Create checkmark overlay checkmark = QLabel("✓", checkbox) checkmark.setStyleSheet(""" QLabel { color: white; background: transparent; font-weight: bold; font-size: 12px; } """) checkmark.setAlignment(Qt.AlignCenter) checkmark.hide() checkmark.setAttribute(Qt.WA_TransparentForMouseEvents) def position_checkmark(): try: if checkmark: checkmark.setGeometry(2, 1, 16, 16) # Increased size from 14x14 to 16x16 except RuntimeError: pass def update_checkmark(): try: if checkbox and checkmark: if checkbox.isChecked(): position_checkmark() checkmark.show() else: checkmark.hide() except RuntimeError: pass checkbox.stateChanged.connect(update_checkmark) def safe_init(): try: position_checkmark() update_checkmark() except RuntimeError: pass QTimer.singleShot(0, safe_init) return checkbox def _build(self): title = f"Configure Individual Endpoint — {getattr(self.key, 'model', '')}" self.setWindowTitle(title) # Use screen ratios for sizing (decreased from 37% x 41% to 30% x 35%) from PySide6.QtWidgets import QApplication screen = QApplication.primaryScreen().geometry() width = int(screen.width() * 0.25) # 30% of screen width height = int(screen.height() * 0.25) # 35% of screen height self.setMinimumSize(width, height) # Main layout main_layout = QVBoxLayout(self) main_layout.setContentsMargins(20, 16, 20, 16) main_layout.setSpacing(10) # Title row with Halgakos icons title_container = QWidget() title_layout = QHBoxLayout(title_container) title_layout.setContentsMargins(0, 0, 0, 0) title_layout.setSpacing(12) title_layout.setAlignment(Qt.AlignCenter) def _load_halgakos_pixmap(logical_size: int = 36): try: base_dir = getattr(self.translator_gui, 'base_dir', os.getcwd()) ico_path = os.path.join(base_dir, 'Halgakos.ico') if not os.path.isfile(ico_path): return None pm = QPixmap(ico_path) if pm.isNull(): return None from PySide6.QtWidgets import QApplication screen = QApplication.primaryScreen() dpr = screen.devicePixelRatio() if screen else 1.0 target = int(logical_size * dpr) scaled = pm.scaled(target, target, Qt.KeepAspectRatio, Qt.SmoothTransformation) scaled.setDevicePixelRatio(dpr) return scaled except Exception: return None icon_left = QLabel() left_pm = _load_halgakos_pixmap(36) if left_pm: icon_left.setPixmap(left_pm) icon_left.setFixedSize(36, 36) icon_left.setAlignment(Qt.AlignCenter) title_layout.addWidget(icon_left, 0, Qt.AlignVCenter) header_label = QLabel("Per-Key Custom Endpoint") header_font = QFont() header_font.setPointSize(14) header_font.setBold(True) header_label.setFont(header_font) header_label.setAlignment(Qt.AlignCenter) title_layout.addWidget(header_label, 0, Qt.AlignVCenter) icon_right = QLabel() right_pm = _load_halgakos_pixmap(36) if right_pm: icon_right.setPixmap(right_pm) icon_right.setFixedSize(36, 36) icon_right.setAlignment(Qt.AlignCenter) title_layout.addWidget(icon_right, 0, Qt.AlignVCenter) main_layout.addWidget(title_container) # Header (enable toggle + spinner) header_layout = QHBoxLayout() header_layout.setContentsMargins(0, 0, 0, 0) # Enable toggle using styled checkbox with icon self.enable_checkbox = self._create_styled_checkbox("Enable") self.enable_checkbox.setChecked(bool(getattr(self.key, 'use_individual_endpoint', False))) self.enable_checkbox.toggled.connect(self._toggle_fields) # Increase checkbox size self.enable_checkbox.setStyleSheet(""" QCheckBox { color: #e0e0e0; spacing: 8px; font-size: 12px; } QCheckBox::indicator { width: 16px; height: 16px; border: 1px solid #5a5a5a; border-radius: 2px; background-color: #2d2d2d; } QCheckBox::indicator:checked { background-color: #4a7ba7; border-color: #4a7ba7; } QCheckBox::indicator:hover { border-color: #6a6a6a; } """) header_layout.addStretch() # Add Halgakos.ico next to the enable checkbox using HiDPI pixmap icon_label = QLabel() toggle_pm = _load_halgakos_pixmap(24) if toggle_pm: icon_label.setPixmap(toggle_pm) icon_label.setFixedSize(24, 24) icon_label.setAlignment(Qt.AlignCenter) # Connect checkbox toggle to spinning animation (uses same label) self.enable_checkbox.toggled.connect(lambda: animate_icon(icon_label)) header_layout.addWidget(icon_label) header_layout.addWidget(self.enable_checkbox) main_layout.addLayout(header_layout) # Description desc = ( "Use a custom endpoint for this API key only. Works with OpenAI-compatible servers\n" "like Azure OpenAI or local providers (e.g., Ollama at http://localhost:11434/v1)." ) desc_label = QLabel(desc) desc_label.setStyleSheet("color: gray;") desc_label.setWordWrap(True) main_layout.addWidget(desc_label) # Form group form_group = QGroupBox("Endpoint Settings") form_layout = QGridLayout(form_group) form_layout.setContentsMargins(14, 12, 14, 12) form_layout.setSpacing(6) # Endpoint URL self.endpoint_label = QLabel("Endpoint Base URL:") form_layout.addWidget(self.endpoint_label, 0, 0, Qt.AlignLeft) self.endpoint_entry = QLineEdit() self.endpoint_entry.setText(getattr(self.key, 'azure_endpoint', '') or '') form_layout.addWidget(self.endpoint_entry, 0, 1) # Azure API version self.api_version_label = QLabel("Azure API Version:") form_layout.addWidget(self.api_version_label, 1, 0, Qt.AlignLeft) self.api_version_combo = QComboBox() self.api_version_combo.addItems([ '2025-01-01-preview', '2024-12-01-preview', '2024-10-01-preview', '2024-08-01-preview', '2024-06-01', '2024-02-01', '2023-12-01-preview' ]) current_version = getattr(self.key, 'azure_api_version', '2025-01-01-preview') or '2025-01-01-preview' index = self.api_version_combo.findText(current_version) if index >= 0: self.api_version_combo.setCurrentIndex(index) form_layout.addWidget(self.api_version_combo, 1, 1) # Helper text hint = ( "Hints:\n" "- Ollama: http://localhost:11434/v1\n" "- Azure OpenAI: https://.openai.azure.com/ (version required)\n" "- Other OpenAI-compatible: Provide the base URL ending with /v1 if applicable" ) hint_label = QLabel(hint) hint_label.setStyleSheet("color: gray; font-size: 9pt;") hint_label.setWordWrap(True) form_layout.addWidget(hint_label, 2, 0, 1, 2, Qt.AlignLeft) # Make column 1 stretch form_layout.setColumnStretch(1, 1) main_layout.addWidget(form_group) # Buttons button_layout = QHBoxLayout() button_layout.setSpacing(8) disable_button = QPushButton("Disable") disable_button.clicked.connect(self._on_disable) button_layout.addWidget(disable_button) button_layout.addStretch() cancel_button = QPushButton("Cancel") cancel_button.clicked.connect(self._on_close) button_layout.addWidget(cancel_button) save_button = QPushButton("Save") save_button.setDefault(True) save_button.clicked.connect(self._on_save) button_layout.addWidget(save_button) main_layout.addLayout(button_layout) # Initial toggle state self._toggle_fields() def _toggle_fields(self): enabled = self.enable_checkbox.isChecked() # Enable/disable input fields self.endpoint_entry.setEnabled(enabled) self.api_version_combo.setEnabled(enabled) # Enable/disable labels with color change label_color = 'white' if enabled else 'gray' self.endpoint_label.setEnabled(enabled) self.endpoint_label.setStyleSheet(f"color: {label_color};") self.api_version_label.setEnabled(enabled) self.api_version_label.setStyleSheet(f"color: {label_color};") def _is_azure_endpoint(self, url: str) -> bool: if not url: return False url_l = url.lower() return (".openai.azure.com" in url_l) or ("azure.com/openai" in url_l) or ("/openai/deployments/" in url_l) def _validate(self) -> bool: if not self.enable_checkbox.isChecked(): return True url = self.endpoint_entry.text().strip() if not url: QMessageBox.critical(self, "Validation Error", "Endpoint Base URL is required when Enable is ON.") return False if not (url.startswith("http://") or url.startswith("https://")): QMessageBox.critical(self, "Validation Error", "Endpoint URL must start with http:// or https://") return False if self._is_azure_endpoint(url): ver = self.api_version_combo.currentText().strip() if not ver: QMessageBox.critical(self, "Validation Error", "Azure API Version is required for Azure endpoints.") return False return True def _persist_to_config_if_possible(self): """Best-effort persistence: update translator_gui.config['multi_api_keys'] for this key entry. We match by api_key and model to find the entry. If not found, skip silently. """ try: cfg = getattr(self.translator_gui, 'config', None) if not isinstance(cfg, dict): return key_list = cfg.get('multi_api_keys', []) # Find by api_key AND model (best-effort) api_key = getattr(self.key, 'api_key', None) model = getattr(self.key, 'model', None) for entry in key_list: if entry.get('api_key') == api_key and entry.get('model') == model: entry['use_individual_endpoint'] = bool(getattr(self.key, 'use_individual_endpoint', False)) entry['azure_endpoint'] = getattr(self.key, 'azure_endpoint', None) entry['azure_api_version'] = getattr(self.key, 'azure_api_version', None) break # Save without message if hasattr(self.translator_gui, 'save_config'): self.translator_gui.save_config(show_message=False) except Exception: # Non-fatal pass def _on_save(self): if not self._validate(): return enabled = self.enable_checkbox.isChecked() url = self.endpoint_entry.text().strip() ver = self.api_version_combo.currentText().strip() # Apply to key object self.key.use_individual_endpoint = enabled self.key.azure_endpoint = url if enabled else None # Keep API version even if disabled, but it's only used when enabled self.key.azure_api_version = ver or getattr(self.key, 'azure_api_version', '2025-01-01-preview') # Notify parent UI if callable(self.refresh_callback): try: self.refresh_callback() except Exception: pass if callable(self.status_callback): try: if enabled and url: self.status_callback(f"Individual endpoint set: {url}") else: self.status_callback("Individual endpoint disabled") except Exception: pass # Best-effort persistence to config self._persist_to_config_if_possible() self.accept() def _on_disable(self): # Disable quickly self.enable_checkbox.setChecked(False) self._toggle_fields() # Apply immediately and close self._on_save() def _on_close(self): self.reject()