1→import SwiftUI 2→ 3→struct SettingsView: View { 4→ @State private var claudeAPIKey: String = "" 5→ @State private var lmstudioURL: String = AppConstants.AI.defaultLMStudioURL 6→ @State private var lmstudioAPIKey: String = "" 7→ @State private var showClaudeKey: Bool = false 8→ @State private var showLMStudioKey: Bool = false 9→ @State private var isTesting: Bool = false 10→ @State private var testResult: TestResult? 11→ 12→ enum TestResult { 13→ case success(String) 14→ case failure(String) 15→ } 16→ 17→ var body: some View { 18→ Form { 19→ // Claude API Section 20→ Section { 21→ VStack(alignment: .leading, spacing: 12) { 22→ HStack { 23→ Image(systemName: "brain") 24→ .foregroundStyle(.purple) 25→ Text("Claude API") 26→ .font(.headline) 27→ 28→ Spacer() 29→ 30→ if KeychainService.hasClaudeAPIKey { 31→ Label("Connected", systemImage: "checkmark.circle.fill") 32→ .foregroundStyle(.green) 33→ .font(.caption) 34→ } 35→ } 36→ 37→ Text("Primary AI provider. Get your API key from console.anthropic.com") 38→ .font(.caption) 39→ .foregroundStyle(.secondary) 40→ 41→ HStack { 42→ Group { 43→ if showClaudeKey { 44→ TextField("sk-ant-api03-...", text: $claudeAPIKey) 45→ } else { 46→ SecureField("sk-ant-api03-...", text: $claudeAPIKey) 47→ } 48→ } 49→ .textFieldStyle(.roundedBorder) 50→ 51→ Button { 52→ showClaudeKey.toggle() 53→ } label: { 54→ Image(systemName: showClaudeKey ? "eye.slash" : "eye") 55→ } 56→ .buttonStyle(.borderless) 57→ 58→ Button("Save") { 59→ saveClaudeAPIKey() 60→ } 61→ .disabled(claudeAPIKey.isEmpty) 62→ 63→ Button("Test") { 64→ testClaudeConnection() 65→ } 66→ .disabled(claudeAPIKey.isEmpty && !KeychainService.hasClaudeAPIKey) 67→ } 68→ } 69→ } header: { 70→ Text("AI Providers") 71→ } 72→ 73→ // LM Studio Section 74→ Section { 75→ VStack(alignment: .leading, spacing: 12) { 76→ HStack { 77→ Image(systemName: "server.rack") 78→ .foregroundStyle(.blue) 79→ Text("LM Studio") 80→ .font(.headline) 81→ 82→ Spacer() 83→ 84→ Text("Fallback") 85→ .font(.caption) 86→ .foregroundStyle(.secondary) 87→ .padding(.horizontal, 8) 88→ .padding(.vertical, 2) 89→ .background(.secondary.opacity(0.2)) 90→ .cornerRadius(4) 91→ } 92→ 93→ Text("Local AI server for offline use. Optional API key for LM Gateway.") 94→ .font(.caption) 95→ .foregroundStyle(.secondary) 96→ 97→ TextField("Server URL", text: $lmstudioURL) 98→ .textFieldStyle(.roundedBorder) 99→ 100→ HStack { 101→ Group { 102→ if showLMStudioKey { 103→ TextField("API Key (optional)", text: $lmstudioAPIKey) 104→ } else { 105→ SecureField("API Key (optional)", text: $lmstudioAPIKey) 106→ } 107→ } 108→ .textFieldStyle(.roundedBorder) 109→ 110→ Button { 111→ showLMStudioKey.toggle() 112→ } label: { 113→ Image(systemName: showLMStudioKey ? "eye.slash" : "eye") 114→ } 115→ .buttonStyle(.borderless) 116→ 117→ Button("Save") { 118→ saveLMStudioSettings() 119→ } 120→ 121→ Button("Test") { 122→ testLMStudioConnection() 123→ } 124→ } 125→ } 126→ } 127→ 128→ // Test Result 129→ if let result = testResult { 130→ Section { 131→ HStack { 132→ switch result { 133→ case .success(let message): 134→ Image(systemName: "checkmark.circle.fill") 135→ .foregroundStyle(.green) 136→ Text(message) 137→ .foregroundStyle(.green) 138→ case .failure(let message): 139→ Image(systemName: "xmark.circle.fill") 140→ .foregroundStyle(.red) 141→ Text(message) 142→ .foregroundStyle(.red) 143→ } 144→ } 145→ } 146→ } 147→ 148→ // About Section 149→ Section { 150→ HStack { 151→ Text("Version") 152→ Spacer() 153→ Text(AppConstants.App.version) 154→ .foregroundStyle(.secondary) 155→ } 156→ 157→ HStack { 158→ Text("Model") 159→ Spacer() 160→ Text(AppConstants.AI.defaultClaudeModel) 161→ .foregroundStyle(.secondary) 162→ .font(.caption) 163→ } 164→ 165→ Link(destination: URL(string: "https://console.anthropic.com")!) { 166→ HStack { 167→ Text("Get Claude API Key") 168→ Spacer() 169→ Image(systemName: "arrow.up.right.square") 170→ } 171→ } 172→ } header: { 173→ Text("About") 174→ } 175→ } 176→ .formStyle(.grouped) 177→ .frame(minWidth: 500, minHeight: 400) 178→ .padding() 179→ .onAppear { 180→ loadSettings() 181→ } 182→ } 183→ 184→ // MARK: - Actions 185→ 186→ private func loadSettings() { 187→ if let key = KeychainService.claudeAPIKey { 188→ claudeAPIKey = key 189→ } 190→ lmstudioURL = KeychainService.lmstudioURL 191→ if let key = KeychainService.lmstudioAPIKey { 192→ lmstudioAPIKey = key 193→ } 194→ } 195→ 196→ private func saveClaudeAPIKey() { 197→ KeychainService.claudeAPIKey = claudeAPIKey 198→ testResult = .success("Claude API key saved!") 199→ } 200→ 201→ private func saveLMStudioSettings() { 202→ KeychainService.lmstudioURL = lmstudioURL 203→ if !lmstudioAPIKey.isEmpty { 204→ KeychainService.lmstudioAPIKey = lmstudioAPIKey 205→ } 206→ testResult = .success("LM Studio settings saved!") 207→ } 208→ 209→ private func testClaudeConnection() { 210→ isTesting = true 211→ testResult = nil 212→ 213→ Task { 214→ do { 215→ // Save key first if not already saved 216→ if !claudeAPIKey.isEmpty { 217→ KeychainService.claudeAPIKey = claudeAPIKey 218→ } 219→ 220→ let response = try await ClaudeService.shared.generate( 221→ prompt: "Say 'Connection successful!' in exactly those words.", 222→ maxTokens: 20 223→ ) 224→ 225→ await MainActor.run { 226→ testResult = .success("Claude connected: \(response.prefix(50))") 227→ isTesting = false 228→ } 229→ } catch { 230→ await MainActor.run { 231→ testResult = .failure(error.localizedDescription) 232→ isTesting = false 233→ } 234→ } 235→ } 236→ } 237→ 238→ private func testLMStudioConnection() { 239→ isTesting = true 240→ testResult = nil 241→ 242→ Task { 243→ // Save settings first 244→ KeychainService.lmstudioURL = lmstudioURL 245→ if !lmstudioAPIKey.isEmpty { 246→ KeychainService.lmstudioAPIKey = lmstudioAPIKey 247→ } 248→ 249→ let isAvailable = await LMStudioService.shared.isAvailable() 250→ 251→ await MainActor.run { 252→ if isAvailable { 253→ testResult = .success("LM Studio connected!") 254→ } else { 255→ testResult = .failure("Could not connect to LM Studio at \(lmstudioURL)") 256→ } 257→ isTesting = false 258→ } 259→ } 260→ } 261→} 262→ 263→#Preview { 264→ SettingsView() 265→} 266→ Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.