Skip to content

PR 12: Settings UI - Provider Preferences #13

Description

@gilmanb1

Overview

Add AI provider preference settings to the Settings view, including provider selection dropdown, quality thresholds, and greyed-out state handling for settings not supported by the current provider.

Dependencies

Files to Modify

File Action Description
Sources/SortAI/App/SettingsView.swift Modify Add AI provider section
Sources/SortAI/Core/Configuration/AppConfiguration.swift Modify Add AIProviderConfiguration

Implementation Details

1. AIProviderConfiguration

/// Configuration for AI provider settings
struct AIProviderConfiguration: Codable, Sendable {
    var preference: ProviderPreference = .automatic
    var escalationThreshold: Double = 0.5
    var autoAcceptThreshold: Double = 0.7
    var autoInstallOllama: Bool = true
    
    // Ollama-specific
    var ollamaServerURL: String = "http://127.0.0.1:11434"
    var ollamaModel: String = "deepseek-r1:8b"
    var ollamaAutoDownload: Bool = true
    
    // Cloud-specific
    var cloudProvider: CloudProvider = .openai
    var openAIApiKey: String = ""
    var openAIModel: String = "gpt-4o-mini"
    var anthropicApiKey: String = ""
    var anthropicModel: String = "claude-3-haiku"
    
    enum CloudProvider: String, Codable, CaseIterable {
        case openai = "openai"
        case anthropic = "anthropic"
        
        var displayName: String {
            switch self {
            case .openai: return "OpenAI"
            case .anthropic: return "Anthropic"
            }
        }
    }
}

2. Settings UI Section

import SwiftUI

struct AIProviderSettingsSection: View {
    @Binding var config: AIProviderConfiguration
    @State private var settingsAvailability: ProviderSettingsAvailability = .allEnabled
    @State private var availableProviders: [String] = []
    
    var body: some View {
        Section("AI Provider") {
            // Provider preference dropdown
            Picker("LLM Provider", selection: $config.preference) {
                ForEach(ProviderPreference.allCases, id: \.self) { pref in
                    VStack(alignment: .leading) {
                        Text(pref.displayName)
                        Text(pref.description)
                            .font(.caption)
                            .foregroundColor(.secondary)
                    }
                    .tag(pref)
                }
            }
            .pickerStyle(.menu)
            
            // Active provider indicator
            HStack {
                Text("Active Provider")
                Spacer()
                ProviderBadge(provider: activeProvider)
            }
            
            // Quality thresholds
            VStack(alignment: .leading, spacing: 8) {
                HStack {
                    Text("Auto-accept threshold")
                    Spacer()
                    Text(String(format: "%.0f%%", config.autoAcceptThreshold * 100))
                        .foregroundColor(.secondary)
                }
                Slider(value: $config.autoAcceptThreshold, in: 0.5...1.0, step: 0.05)
                
                Text("Files with confidence above this will be auto-accepted")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
            
            VStack(alignment: .leading, spacing: 8) {
                HStack {
                    Text("Escalation threshold")
                    Spacer()
                    Text(String(format: "%.0f%%", config.escalationThreshold * 100))
                        .foregroundColor(.secondary)
                }
                Slider(value: $config.escalationThreshold, in: 0.1...0.9, step: 0.05)
                    .disabled(config.preference != .automatic)
                
                Text("Results below this will try the next provider (Automatic mode only)")
                    .font(.caption)
                    .foregroundColor(.secondary)
            }
        }
        
        // Ollama settings (greyed out when using Apple Intelligence only)
        Section {
            ollamaSettingsContent
        } header: {
            HStack {
                Text("Ollama Settings")
                if !isOllamaSettingsEnabled {
                    Spacer()
                    Image(systemName: "info.circle")
                        .foregroundColor(.secondary)
                        .help("Not available when using Apple Intelligence Only mode")
                }
            }
        }
        .disabled(!isOllamaSettingsEnabled)
        .opacity(isOllamaSettingsEnabled ? 1.0 : 0.5)
        
        // Cloud settings (greyed out unless cloud mode)
        Section {
            cloudSettingsContent
        } header: {
            HStack {
                Text("Cloud Settings")
                if !isCloudSettingsEnabled {
                    Spacer()
                    Image(systemName: "info.circle")
                        .foregroundColor(.secondary)
                        .help("Only available in Cloud mode")
                }
            }
        }
        .disabled(!isCloudSettingsEnabled)
        .opacity(isCloudSettingsEnabled ? 1.0 : 0.5)
    }
    
    // MARK: - Ollama Settings
    
    @ViewBuilder
    private var ollamaSettingsContent: some View {
        TextField("Server URL", text: $config.ollamaServerURL)
            .textFieldStyle(.roundedBorder)
        
        Picker("Model", selection: $config.ollamaModel) {
            Text("deepseek-r1:8b").tag("deepseek-r1:8b")
            Text("llama3.2").tag("llama3.2")
            Text("llama3.1").tag("llama3.1")
            Text("mistral").tag("mistral")
            Text("phi3").tag("phi3")
        }
        .disabled(!settingsAvailability.modelSelection)
        
        Toggle("Auto-download missing models", isOn: $config.ollamaAutoDownload)
        
        Toggle("Auto-install Ollama if not found", isOn: $config.autoInstallOllama)
    }
    
    // MARK: - Cloud Settings
    
    @ViewBuilder
    private var cloudSettingsContent: some View {
        Picker("Provider", selection: $config.cloudProvider) {
            ForEach(AIProviderConfiguration.CloudProvider.allCases, id: \.self) { provider in
                Text(provider.displayName).tag(provider)
            }
        }
        
        switch config.cloudProvider {
        case .openai:
            SecureField("API Key", text: $config.openAIApiKey)
                .textFieldStyle(.roundedBorder)
            
            Picker("Model", selection: $config.openAIModel) {
                Text("gpt-4o-mini").tag("gpt-4o-mini")
                Text("gpt-4o").tag("gpt-4o")
                Text("gpt-4-turbo").tag("gpt-4-turbo")
            }
            
        case .anthropic:
            SecureField("API Key", text: $config.anthropicApiKey)
                .textFieldStyle(.roundedBorder)
            
            Picker("Model", selection: $config.anthropicModel) {
                Text("claude-3-haiku").tag("claude-3-haiku")
                Text("claude-3-sonnet").tag("claude-3-sonnet")
                Text("claude-3-opus").tag("claude-3-opus")
            }
        }
    }
    
    // MARK: - Computed Properties
    
    private var isOllamaSettingsEnabled: Bool {
        config.preference != .appleIntelligenceOnly
    }
    
    private var isCloudSettingsEnabled: Bool {
        config.preference == .cloud
    }
    
    private var activeProvider: String {
        // This would come from UnifiedCategorizationService
        switch config.preference {
        case .automatic: return "apple-intelligence"
        case .appleIntelligenceOnly: return "apple-intelligence"
        case .preferOllama: return "ollama"
        case .cloud: return config.cloudProvider.rawValue
        }
    }
}

3. ProviderBadge Component

This is a small badge component showing which provider is active (can also be PR 13):

struct ProviderBadge: View {
    let provider: String
    
    var body: some View {
        HStack(spacing: 4) {
            Image(systemName: iconName)
            Text(displayName)
        }
        .font(.caption)
        .padding(.horizontal, 8)
        .padding(.vertical, 4)
        .background(backgroundColor.opacity(0.2))
        .foregroundColor(backgroundColor)
        .cornerRadius(8)
    }
    
    private var displayName: String {
        switch provider {
        case "apple-intelligence": return "Apple Intelligence"
        case "ollama": return "Ollama"
        case "openai": return "OpenAI"
        case "anthropic": return "Anthropic"
        case "local-ml": return "Local ML"
        default: return provider.capitalized
        }
    }
    
    private var iconName: String {
        switch provider {
        case "apple-intelligence": return "apple.logo"
        case "ollama": return "server.rack"
        case "openai", "anthropic": return "cloud"
        case "local-ml": return "cpu"
        default: return "questionmark.circle"
        }
    }
    
    private var backgroundColor: Color {
        switch provider {
        case "apple-intelligence": return .blue
        case "ollama": return .green
        case "openai": return .teal
        case "anthropic": return .orange
        case "local-ml": return .purple
        default: return .gray
        }
    }
}

4. Integration with AppConfiguration

// In AppConfiguration.swift
extension AppConfiguration {
    var aiProvider: AIProviderConfiguration {
        get {
            // Load from UserDefaults or default
            guard let data = UserDefaults.standard.data(forKey: "aiProviderConfig"),
                  let config = try? JSONDecoder().decode(AIProviderConfiguration.self, from: data) else {
                return AIProviderConfiguration()
            }
            return config
        }
        set {
            if let data = try? JSONEncoder().encode(newValue) {
                UserDefaults.standard.set(data, forKey: "aiProviderConfig")
            }
        }
    }
}

UI Design

┌─────────────────────────────────────────────────────────────────┐
│  AI Provider                                                     │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  LLM Provider: [Automatic (Recommended)]  ▼                     │
│                                                                 │
│  Active Provider:    ┌──────────────────┐                       │
│                      │ 🍎 Apple Intel │                       │
│                      └──────────────────┘                       │
│                                                                 │
│  Auto-accept threshold:                          70%            │
│  ────────────────────────────●─────────────                     │
│  Files above this confidence are auto-accepted                  │
│                                                                 │
│  Escalation threshold:                           50%            │
│  ─────────────●────────────────────────────                     │
│  Results below this try the next provider                       │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│  Ollama Settings                               ⓘ (greyed out)  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Server URL: [http://127.0.0.1:11434]                           │
│  Model: [deepseek-r1:8b] ▼                                      │
│  ☑ Auto-download missing models                                 │
│  ☑ Auto-install Ollama if not found                             │
│                                                                 │
├─────────────────────────────────────────────────────────────────┤
│  Cloud Settings                                ⓘ (greyed out)  │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Provider: [OpenAI] ▼                                           │
│  API Key: [••••••••••••••••]                                    │
│  Model: [gpt-4o-mini] ▼                                         │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Acceptance Criteria

  • Provider preference dropdown shows all options
  • Active provider badge displays correctly
  • Quality threshold sliders work correctly
  • Ollama settings greyed out in Apple Intelligence Only mode
  • Cloud settings greyed out except in Cloud mode
  • Tooltip explains why settings are disabled
  • Settings persist to UserDefaults
  • API keys are stored securely

Testing

func testProviderPreferenceDropdown() {
    let view = AIProviderSettingsSection(config: .constant(AIProviderConfiguration()))
    
    // Verify all options present
    XCTAssertEqual(ProviderPreference.allCases.count, 4)
}

func testOllamaSettingsDisabledInAppleOnlyMode() {
    var config = AIProviderConfiguration()
    config.preference = .appleIntelligenceOnly
    
    let view = AIProviderSettingsSection(config: .constant(config))
    
    // Ollama settings should be disabled
    // (UI testing would verify this)
}

func testConfigurationPersistence() {
    var config = AIProviderConfiguration()
    config.preference = .preferOllama
    config.escalationThreshold = 0.6
    
    // Save
    let appConfig = AppConfiguration.shared
    appConfig.aiProvider = config
    
    // Reload
    let loaded = appConfig.aiProvider
    XCTAssertEqual(loaded.preference, .preferOllama)
    XCTAssertEqual(loaded.escalationThreshold, 0.6)
}

Estimated Size

~150 lines of code

Risk Assessment

Low - Standard SwiftUI settings view. Well-established patterns.

Activity

  1. added
    enhancementNew feature or request
    uiUser interface changes
    phase-3Phase 3 - Integration
    on Jan 13, 2026
  2. gilmanb1 commented on Jan 13, 2026

    @gilmanb1
    OwnerAuthor

    Dependencies: Blocked by #2, #6

  3. gilmanb1 commented on Jan 13, 2026

    @gilmanb1
    OwnerAuthor

    ✅ Implementation Complete

    This feature was already implemented as part of the LLM_Research.md implementation. The code exists in the codebase and all 233 tests pass.

    A minor build fix was applied in commit 4341283 to fix the Apple Intelligence availability check API.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestphase-3Phase 3 - IntegrationuiUser interface changes

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions