vOOice/VoiceInk/Views/AI Models/APIKeyManagementView.swift
Beingpax ae387ad351 Improve API key management UI and enhance provider integration layout
- Reorganize provider connection status display
- Add visual separators between sections for better clarity
- Improve OpenRouter model picker with inline refresh button
- Enhance API key input styling with rounded borders
- Optimize Ollama configuration layout for consistency
- Refine button positioning and spacing throughout
2026-01-05 11:15:11 +05:45

295 lines
13 KiB
Swift

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 {
Picker("Model", selection: $selectedOllamaModel) {
ForEach(ollamaModels) { model in
Text(model.name).tag(model.name)
}
}
.onChange(of: selectedOllamaModel) { oldValue, newValue in
aiService.updateSelectedOllamaModel(newValue)
}
}
Button("Refresh Connection") {
checkOllamaConnection()
}
} else if aiService.selectedProvider == .custom {
// Custom Configuration inline
TextField("API Endpoint URL", text: $aiService.customBaseURL)
TextField("Model Name", text: $aiService.customModel)
if aiService.isAPIKeyValid {
HStack {
Text("API Key Set")
Spacer()
Button("Remove Key", role: .destructive) {
aiService.clearAPIKey()
}
}
} else {
SecureField("API Key", text: $apiKey)
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
}
}
}