"""Settings widget for AI provider configuration.""" from __future__ import annotations import webbrowser from gi.repository import GLib, Gtk from ignis import widgets from .provider_client import ( OllamaProvider, GeminiProvider, OpenRouterProvider, CopilotProvider, ) from .reasoning_controller import ReasoningController class SettingsWidget(widgets.Box): """Settings view for configuring AI providers.""" def __init__(self, preferences: ReasoningController, on_provider_changed=None, on_back=None): """Initialize settings widget. Args: preferences: ReasoningController instance for managing preferences on_provider_changed: Callback function called when provider changes on_back: Callback function called when back button is clicked """ self._preferences = preferences self._on_provider_changed = on_provider_changed self._on_back_callback = on_back # Provider instances (will be created as needed) self._providers = {} # Header header_title = widgets.Label( label="Settings", halign="start", css_classes=["title-2"], ) back_button = widgets.Button( label="← Back", on_click=lambda x: self._on_back(), halign="start", hexpand=False, ) header_box = widgets.Box( spacing=8, hexpand=True, child=[back_button, header_title], ) # Provider selection provider_label = widgets.Label( label="AI Provider", halign="start", css_classes=["title-3"], ) # Radio buttons for provider selection self._provider_buttons = {} provider_names = { "ollama": "Ollama (Local)", "gemini": "Google Gemini", "openrouter": "OpenRouter", "copilot": "GitHub Copilot", } current_provider = self._preferences.get_provider() provider_box = widgets.Box(vertical=True, spacing=4) for provider_id, provider_label_text in provider_names.items(): button = Gtk.CheckButton(label=provider_label_text) button.set_active(provider_id == current_provider) button.connect("toggled", lambda btn, pid=provider_id: self._on_provider_selected(pid) if btn.get_active() else None) self._provider_buttons[provider_id] = button provider_box.append(button) # API key inputs self._api_key_entries = {} # Gemini API key gemini_label = widgets.Label( label="Gemini API Key", halign="start", ) gemini_entry = Gtk.Entry() gemini_entry.set_placeholder_text("Enter your Gemini API key") gemini_entry.set_visibility(False) # Password mode gemini_entry.set_input_purpose(Gtk.InputPurpose.PASSWORD) api_key = self._preferences.get_api_key("gemini") if api_key: gemini_entry.set_text(api_key) gemini_entry.connect("changed", lambda e: self._on_api_key_changed("gemini", e.get_text())) self._api_key_entries["gemini"] = gemini_entry # Toggle visibility button for Gemini gemini_visibility_button = Gtk.Button(icon_name="view-reveal-symbolic") gemini_visibility_button.connect("clicked", lambda b: self._toggle_password_visibility(gemini_entry, b)) gemini_box = widgets.Box( spacing=8, hexpand=True, child=[gemini_entry, gemini_visibility_button], ) # OpenRouter API key openrouter_label = widgets.Label( label="OpenRouter API Key", halign="start", ) openrouter_entry = Gtk.Entry() openrouter_entry.set_placeholder_text("Enter your OpenRouter API key") openrouter_entry.set_visibility(False) # Password mode openrouter_entry.set_input_purpose(Gtk.InputPurpose.PASSWORD) api_key = self._preferences.get_api_key("openrouter") if api_key: openrouter_entry.set_text(api_key) openrouter_entry.connect("changed", lambda e: self._on_api_key_changed("openrouter", e.get_text())) self._api_key_entries["openrouter"] = openrouter_entry # Toggle visibility button for OpenRouter openrouter_visibility_button = Gtk.Button(icon_name="view-reveal-symbolic") openrouter_visibility_button.connect("clicked", lambda b: self._toggle_password_visibility(openrouter_entry, b)) openrouter_box = widgets.Box( spacing=8, hexpand=True, child=[openrouter_entry, openrouter_visibility_button], ) # GitHub Copilot OAuth copilot_label = widgets.Label( label="GitHub Copilot", halign="start", ) copilot_status_label = widgets.Label( label="Not authenticated" if not self._preferences.get_copilot_token() else "Authenticated", halign="start", css_classes=["dim-label"], ) copilot_auth_button = widgets.Button( label="Authenticate with GitHub" if not self._preferences.get_copilot_token() else "Re-authenticate", on_click=lambda x: self._on_copilot_auth(), ) # Test connection buttons test_ollama_button = widgets.Button( label="Test Connection", on_click=lambda x: self._test_connection("ollama"), ) test_gemini_button = widgets.Button( label="Test Connection", on_click=lambda x: self._test_connection("gemini"), ) test_openrouter_button = widgets.Button( label="Test Connection", on_click=lambda x: self._test_connection("openrouter"), ) test_copilot_button = widgets.Button( label="Test Connection", on_click=lambda x: self._test_connection("copilot"), ) # Status labels for test results self._test_status_labels = { "ollama": widgets.Label(label="", halign="start", css_classes=["dim-label"]), "gemini": widgets.Label(label="", halign="start", css_classes=["dim-label"]), "openrouter": widgets.Label(label="", halign="start", css_classes=["dim-label"]), "copilot": widgets.Label(label="", halign="start", css_classes=["dim-label"]), } # Layout settings_content = widgets.Box( vertical=True, spacing=16, hexpand=True, vexpand=True, ) # Provider section settings_content.append(provider_label) settings_content.append(provider_box) spacer1 = widgets.Box(hexpand=True) spacer1.set_size_request(-1, 12) settings_content.append(spacer1) # Gemini section settings_content.append(gemini_label) settings_content.append(gemini_box) test_gemini_box = widgets.Box(spacing=8, hexpand=True, child=[test_gemini_button, self._test_status_labels["gemini"]]) settings_content.append(test_gemini_box) spacer2 = widgets.Box(hexpand=True) spacer2.set_size_request(-1, 8) settings_content.append(spacer2) # OpenRouter section settings_content.append(openrouter_label) settings_content.append(openrouter_box) test_openrouter_box = widgets.Box(spacing=8, hexpand=True, child=[test_openrouter_button, self._test_status_labels["openrouter"]]) settings_content.append(test_openrouter_box) spacer3 = widgets.Box(hexpand=True) spacer3.set_size_request(-1, 8) settings_content.append(spacer3) # Ollama section ollama_label = widgets.Label( label="Ollama", halign="start", ) ollama_info = widgets.Label( label="Local Ollama server. Start with: ollama serve", halign="start", css_classes=["dim-label"], ) settings_content.append(ollama_label) settings_content.append(ollama_info) test_ollama_box = widgets.Box(spacing=8, hexpand=True, child=[test_ollama_button, self._test_status_labels["ollama"]]) settings_content.append(test_ollama_box) spacer4 = widgets.Box(hexpand=True) spacer4.set_size_request(-1, 8) settings_content.append(spacer4) # Copilot section settings_content.append(copilot_label) settings_content.append(copilot_status_label) settings_content.append(copilot_auth_button) test_copilot_box = widgets.Box(spacing=8, hexpand=True, child=[test_copilot_button, self._test_status_labels["copilot"]]) settings_content.append(test_copilot_box) # Scrolled window for settings scroller = widgets.Scroll( hexpand=True, vexpand=True, child=settings_content, ) # Main container super().__init__( vertical=True, spacing=12, hexpand=True, vexpand=True, child=[header_box, scroller], css_classes=["ai-sidebar-content"], ) # Set margins self.set_margin_top(16) self.set_margin_bottom(16) self.set_margin_start(16) self.set_margin_end(16) def _toggle_password_visibility(self, entry: Gtk.Entry, button: Gtk.Button): """Toggle password visibility in entry.""" visible = entry.get_visibility() entry.set_visibility(not visible) button.set_icon_name("view-reveal-symbolic" if visible else "view-conceal-symbolic") def _on_provider_selected(self, provider_id: str): """Handle provider selection.""" self._preferences.set_provider(provider_id) if self._on_provider_changed: self._on_provider_changed(provider_id) def _on_api_key_changed(self, provider: str, api_key: str): """Handle API key changes.""" self._preferences.set_api_key(provider, api_key) # Clear test status self._test_status_labels[provider].label = "" def _on_copilot_auth(self): """Handle GitHub Copilot OAuth authentication.""" # Note: For a full OAuth implementation, you would need: # 1. A GitHub OAuth app registered # 2. A local HTTP server to receive the callback # 3. Exchange authorization code for token # For now, we'll show a message that OAuth setup is required status_label = self._test_status_labels["copilot"] status_label.label = "OAuth setup required. See documentation for GitHub OAuth app configuration." status_label.css_classes = ["dim-label"] # TODO: Implement full OAuth flow # This would involve: # 1. Generate OAuth URL # 2. Open browser # 3. Start local server to receive callback # 4. Exchange code for token # 5. Save token def _test_connection(self, provider_id: str): """Test connection to a provider.""" status_label = self._test_status_labels[provider_id] status_label.label = "Testing..." status_label.css_classes = ["dim-label"] def _test(): try: provider = self._get_provider(provider_id) if provider: success, message = provider.test_connection() GLib.idle_add( lambda: self._update_test_status(provider_id, success, message), priority=GLib.PRIORITY_DEFAULT ) else: GLib.idle_add( lambda: self._update_test_status(provider_id, False, "Provider not configured"), priority=GLib.PRIORITY_DEFAULT ) except Exception as e: GLib.idle_add( lambda: self._update_test_status(provider_id, False, f"Error: {str(e)}"), priority=GLib.PRIORITY_DEFAULT ) import threading thread = threading.Thread(target=_test, daemon=True) thread.start() def _update_test_status(self, provider_id: str, success: bool, message: str): """Update test status label.""" status_label = self._test_status_labels[provider_id] status_label.label = message if success: status_label.css_classes = ["dim-label"] else: status_label.css_classes = ["dim-label"] def _get_provider(self, provider_id: str): """Get or create provider instance.""" if provider_id in self._providers: return self._providers[provider_id] if provider_id == "ollama": provider = OllamaProvider() elif provider_id == "gemini": api_key = self._preferences.get_api_key("gemini") provider = GeminiProvider(api_key=api_key) if api_key else None elif provider_id == "openrouter": api_key = self._preferences.get_api_key("openrouter") provider = OpenRouterProvider(api_key=api_key) if api_key else None elif provider_id == "copilot": token = self._preferences.get_copilot_token() provider = CopilotProvider(oauth_token=token) if token else None else: return None if provider: self._providers[provider_id] = provider return provider def _on_back(self): """Handle back button click.""" # Signal to parent to switch back to chat view if hasattr(self, '_on_back_callback'): self._on_back_callback()