import SwiftUI struct APIKeyManagementView: View { @EnvironmentObject private var aiService: AIService @State private var apiKey: String = "" @State private var showAlert = false @State private var alertMessage = "" @State private var isVerifying = false @State private var ollamaBaseURL: String = UserDefaults.standard.string(forKey: "ollamaBaseURL") ?? "http://localhost:11434" @State private var ollamaModels: [OllamaService.OllamaModel] = [] @State private var selectedOllamaModel: String = UserDefaults.standard.string(forKey: "ollamaSelectedModel") ?? "mistral" @State private var isCheckingOllama = false @State private var isEditingURL = false var body: some View { Section("AI Provider Integration") { HStack { Picker("Provider", selection: $aiService.selectedProvider) { ForEach(AIProvider.allCases.filter { $0 != .elevenLabs && $0 != .deepgram && $0 != .soniox }, id: \.self) { provider in Text(provider.rawValue).tag(provider) } } .pickerStyle(.automatic) .tint(.blue) // Show connected status for all providers if aiService.isAPIKeyValid && aiService.selectedProvider != .ollama { Spacer() Circle() .fill(Color.green) .frame(width: 8, height: 8) Text("Connected") .font(.subheadline) .foregroundColor(.secondary) } else if aiService.selectedProvider == .ollama { Spacer() if isCheckingOllama { ProgressView() .controlSize(.small) } else if !ollamaModels.isEmpty { Circle() .fill(Color.green) .frame(width: 8, height: 8) Text("Connected") .font(.subheadline) .foregroundColor(.secondary) } else { Circle() .fill(Color.red) .frame(width: 8, height: 8) Text("Disconnected") .font(.subheadline) .foregroundColor(.secondary) } } } .onChange(of: aiService.selectedProvider) { oldValue, newValue in if aiService.selectedProvider == .ollama { checkOllamaConnection() } } VStack(alignment: .leading, spacing: 12) { // Model Selection if aiService.selectedProvider == .openRouter { if aiService.availableModels.isEmpty { HStack { Text("No models loaded") .foregroundColor(.secondary) Spacer() Button(action: { Task { await aiService.fetchOpenRouterModels() } }) { Label("Refresh", systemImage: "arrow.clockwise") } } } else { HStack { Picker("Model", selection: Binding( get: { aiService.currentModel }, set: { aiService.selectModel($0) } )) { ForEach(aiService.availableModels, id: \.self) { model in Text(model).tag(model) } } Spacer() Button(action: { Task { await aiService.fetchOpenRouterModels() } }) { Label("Refresh", systemImage: "arrow.clockwise") } } } } else if !aiService.availableModels.isEmpty && aiService.selectedProvider != .ollama && aiService.selectedProvider != .custom { Picker("Model", selection: Binding( get: { aiService.currentModel }, set: { aiService.selectModel($0) } )) { ForEach(aiService.availableModels, id: \.self) { model in Text(model).tag(model) } } } Divider() if aiService.selectedProvider == .ollama { // Ollama Configuration inline if isEditingURL { HStack { TextField("Base URL", text: $ollamaBaseURL) .textFieldStyle(.roundedBorder) Button("Save") { aiService.updateOllamaBaseURL(ollamaBaseURL) checkOllamaConnection() isEditingURL = false } } } else { HStack { Text("Server: \(ollamaBaseURL)") Spacer() Button("Edit") { isEditingURL = true } Button(action: { ollamaBaseURL = "http://localhost:11434" aiService.updateOllamaBaseURL(ollamaBaseURL) checkOllamaConnection() }) { Image(systemName: "arrow.counterclockwise") } .help("Reset to default") } } if !ollamaModels.isEmpty { Divider() Picker("Model", selection: $selectedOllamaModel) { ForEach(ollamaModels) { model in Text(model.name).tag(model.name) } } .onChange(of: selectedOllamaModel) { oldValue, newValue in aiService.updateSelectedOllamaModel(newValue) } } } else if aiService.selectedProvider == .custom { // Custom Configuration inline TextField("API Endpoint URL", text: $aiService.customBaseURL) .textFieldStyle(.roundedBorder) Divider() TextField("Model Name", text: $aiService.customModel) .textFieldStyle(.roundedBorder) Divider() if aiService.isAPIKeyValid { HStack { Text("API Key Set") Spacer() Button("Remove Key", role: .destructive) { aiService.clearAPIKey() } } } else { SecureField("API Key", text: $apiKey) .textFieldStyle(.roundedBorder) Button("Verify and Save") { isVerifying = true aiService.saveAPIKey(apiKey) { success, errorMessage in isVerifying = false if !success { alertMessage = errorMessage ?? "Verification failed" showAlert = true } apiKey = "" } } .disabled(aiService.customBaseURL.isEmpty || aiService.customModel.isEmpty || apiKey.isEmpty) } } else { // API Key Display for other providers if aiService.isAPIKeyValid { HStack { Text("API Key") Spacer() Text("••••••••") .foregroundColor(.secondary) Button("Remove", role: .destructive) { aiService.clearAPIKey() } } } else { SecureField("API Key", text: $apiKey) .textFieldStyle(.roundedBorder) HStack { // Get API Key Link if let url = getAPIKeyURL() { Link(destination: url) { HStack { Image(systemName: "key.fill") Text("Get API Key") } .font(.caption) .foregroundColor(.blue) .padding(.vertical, 4) .padding(.horizontal, 8) .background(Color.blue.opacity(0.1)) .cornerRadius(6) } .buttonStyle(.plain) } Spacer() Button(action: { isVerifying = true aiService.saveAPIKey(apiKey) { success, errorMessage in isVerifying = false if !success { alertMessage = errorMessage ?? "Verification failed" showAlert = true } apiKey = "" } }) { HStack { if isVerifying { ProgressView().controlSize(.small) } Text("Verify and Save") } } .disabled(apiKey.isEmpty) } } } } } .alert("Error", isPresented: $showAlert) { Button("OK", role: .cancel) { } } message: { Text(alertMessage) } .onAppear { if aiService.selectedProvider == .ollama { checkOllamaConnection() } } } private func checkOllamaConnection() { isCheckingOllama = true aiService.checkOllamaConnection { connected in if connected { Task { ollamaModels = await aiService.fetchOllamaModels() isCheckingOllama = false } } else { ollamaModels = [] isCheckingOllama = false alertMessage = "Could not connect to Ollama. Please check if Ollama is running and the base URL is correct." showAlert = true } } } private func getAPIKeyURL() -> URL? { switch aiService.selectedProvider { case .groq: return URL(string: "https://console.groq.com/keys") case .openAI: return URL(string: "https://platform.openai.com/api-keys") case .gemini: return URL(string: "https://makersuite.google.com/app/apikey") case .anthropic: return URL(string: "https://console.anthropic.com/settings/keys") case .mistral: return URL(string: "https://console.mistral.ai/api-keys") case .elevenLabs: return URL(string: "https://elevenlabs.io/speech-synthesis") case .deepgram: return URL(string: "https://console.deepgram.com/api-keys") case .soniox: return URL(string: "https://console.soniox.com/") case .openRouter: return URL(string: "https://openrouter.ai/keys") case .cerebras: return URL(string: "https://cloud.cerebras.ai/") default: return nil } } }