# individual_endpoint_dialog.py """ Individual Endpoint Configuration Dialog for Glossarion - Uses the application's WindowManager for consistent UI - 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 tkinter as tk from tkinter import ttk, messagebox import ttkbootstrap as tb 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: def __init__(self, parent, translator_gui, key, refresh_callback: Callable[[], None], status_callback: Callable[[str], None]): self.parent = parent self.translator_gui = translator_gui self.key = key self.refresh_callback = refresh_callback self.status_callback = status_callback self.dialog = None self.canvas = None self._build() def _build(self): title = f"Configure Individual Endpoint — {getattr(self.key, 'model', '')}" if hasattr(self.translator_gui, 'wm'): # Use WindowManager scrollable dialog for consistency self.dialog, scrollable_frame, self.canvas = self.translator_gui.wm.setup_scrollable( self.parent, title, width=700, height=420, max_width_ratio=0.85, max_height_ratio=0.45 ) else: self.dialog = tk.Toplevel(self.parent) self.dialog.title(title) self.dialog.geometry("700x420") scrollable_frame = self.dialog main = tk.Frame(scrollable_frame, padx=20, pady=16) main.pack(fill=tk.BOTH, expand=True) # Header header = tk.Frame(main) header.pack(fill=tk.X, pady=(0, 10)) tk.Label(header, text="Per-Key Custom Endpoint", font=("TkDefaultFont", 14, "bold")).pack(side=tk.LEFT) # Enable toggle self.enable_var = tk.BooleanVar(value=bool(getattr(self.key, 'use_individual_endpoint', False))) tb.Checkbutton(header, text="Enable", variable=self.enable_var, bootstyle="round-toggle", command=self._toggle_fields).pack(side=tk.RIGHT) # 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)." ) tk.Label(main, text=desc, fg='gray', justify=tk.LEFT).pack(anchor=tk.W) # Form form = tk.LabelFrame(main, text="Endpoint Settings", padx=14, pady=12) form.pack(fill=tk.BOTH, expand=False, pady=(10, 0)) # Endpoint URL tk.Label(form, text="Endpoint Base URL:").grid(row=0, column=0, sticky=tk.W, padx=(0, 10), pady=6) self.endpoint_var = tk.StringVar(value=getattr(self.key, 'azure_endpoint', '') or '') self.endpoint_entry = tb.Entry(form, textvariable=self.endpoint_var) self.endpoint_entry.grid(row=0, column=1, sticky=tk.EW, pady=6) # Azure API version (optional; required if using Azure) tk.Label(form, text="Azure API Version:").grid(row=1, column=0, sticky=tk.W, padx=(0, 10), pady=6) self.api_version_var = tk.StringVar(value=getattr(self.key, 'azure_api_version', '2025-01-01-preview') or '2025-01-01-preview') self.api_version_combo = ttk.Combobox( form, textvariable=self.api_version_var, values=[ '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' ], width=24, state='readonly' ) self.api_version_combo.grid(row=1, column=1, sticky=tk.W, pady=6) # 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" ) tk.Label(form, text=hint, fg='gray', justify=tk.LEFT, font=('TkDefaultFont', 9)).grid( row=2, column=0, columnspan=2, sticky=tk.W, pady=(4, 0) ) # Grid weights form.columnconfigure(1, weight=1) # Buttons btns = tk.Frame(main) btns.pack(fill=tk.X, pady=(14, 0)) tb.Button(btns, text="Save", bootstyle="success", command=self._on_save).pack(side=tk.RIGHT) tb.Button(btns, text="Cancel", bootstyle="secondary", command=self._on_close).pack(side=tk.RIGHT, padx=(0, 8)) tb.Button(btns, text="Disable", bootstyle="danger-outline", command=self._on_disable).pack(side=tk.LEFT) # Initial toggle state self._toggle_fields() # Window close protocol self.dialog.protocol("WM_DELETE_WINDOW", self._on_close) # Auto-size with WM if available if hasattr(self.translator_gui, 'wm') and self.canvas is not None: self.translator_gui.wm.auto_resize_dialog(self.dialog, self.canvas, max_width_ratio=0.9, max_height_ratio=0.45) def _toggle_fields(self): enabled = self.enable_var.get() state = tk.NORMAL if enabled else tk.DISABLED self.endpoint_entry.config(state=state) # API version is only relevant for Azure but we leave it enabled while toggle is on self.api_version_combo.config(state='readonly' if enabled else 'disabled') 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_var.get(): return True url = (self.endpoint_var.get() or '').strip() if not url: messagebox.showerror("Validation Error", "Endpoint Base URL is required when Enable is ON.") return False if not (url.startswith("http://") or url.startswith("https://")): messagebox.showerror("Validation Error", "Endpoint URL must start with http:// or https://") return False if self._is_azure_endpoint(url): ver = (self.api_version_var.get() or '').strip() if not ver: messagebox.showerror("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_var.get() url = (self.endpoint_var.get() or '').strip() ver = (self.api_version_var.get() or '').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.dialog.destroy() def _on_disable(self): # Disable quickly self.enable_var.set(False) self._toggle_fields() # Apply immediately and close self._on_save() def _on_close(self): self.dialog.destroy()