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
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.
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
ProviderPreferenceenum)Files to Modify
Sources/SortAI/App/SettingsView.swiftSources/SortAI/Core/Configuration/AppConfiguration.swiftAIProviderConfigurationImplementation Details
1. AIProviderConfiguration
2. Settings UI Section
3. ProviderBadge Component
This is a small badge component showing which provider is active (can also be PR 13):
4. Integration with AppConfiguration
UI Design
Acceptance Criteria
Testing
Estimated Size
~150 lines of code
Risk Assessment
Low - Standard SwiftUI settings view. Well-established patterns.