import SwiftUI import UniformTypeIdentifiers /// Platform-independent fields; hosts own file access, secret transactions, and request lifetimes. public struct ModelProviderVertexFields: View { @Binding private var isEnabled: Bool @Binding private var projectID: String @Binding private var location: String private let clientEmail: String private let hasCredential: Bool private let requestURL: String? private let readFile: (URL) async throws -> String private let importJSON: (String) throws -> Void private let remove: () throws -> Void @State private var importingFile = false @State private var pasting = false @State private var json = "" @State private var error: String? @State private var importTask: Task? public init( isEnabled: Binding, projectID: Binding, location: Binding, clientEmail: String, hasCredential: Bool, requestURL: String?, readFile: @escaping (URL) async throws -> String, importJSON: @escaping (String) throws -> Void, remove: @escaping () throws -> Void ) { _isEnabled = isEnabled _projectID = projectID _location = location self.clientEmail = clientEmail self.hasCredential = hasCredential self.requestURL = requestURL self.readFile = readFile self.importJSON = importJSON self.remove = remove } public var body: some View { Section("Google Vertex AI") { Toggle("Vertex AI", isOn: $isEnabled) if isEnabled { LabeledContent("Project ID") { TextField("Project ID", text: $projectID.singleLineText, axis: .horizontal) .singleLinePaste().labelsHidden().multilineTextAlignment(.trailing) .autocorrectionDisabled().lineLimit(1) } LabeledContent("Location") { TextField( "Location", text: $location.singleLineText, prompt: Text(verbatim: "global"), axis: .horizontal ) .singleLinePaste().labelsHidden().multilineTextAlignment(.trailing) .autocorrectionDisabled().lineLimit(1) } LabeledContent("Service Account") { if hasCredential { Text(clientEmail).textSelection(.enabled) } else { Text("Required") } } ViewThatFits(in: .horizontal) { HStack { credentialActions } VStack(alignment: .leading, spacing: 12) { credentialActions } } .buttonStyle(.bordered) if let requestURL { LabeledContent("Full URL") { Text(requestURL).font(.caption.monospaced()).textSelection(.enabled) .multilineTextAlignment(.trailing).fixedSize(horizontal: false, vertical: true) } } else { Text("Enter a valid project and location to configure Vertex AI.") .font(.caption).foregroundStyle(.secondary) } Text("Vertex AI requires a service account. Add model IDs manually; model discovery is unavailable.") .font(.caption).foregroundStyle(.secondary) if let error { Text(error).foregroundStyle(.red) } } } .fileImporter(isPresented: $importingFile, allowedContentTypes: [.json], allowsMultipleSelection: false) { result in switch result { case .success(let urls): guard let url = urls.first else { return } importTask?.cancel() importTask = Task { do { let value = try await readFile(url) try Task.checkCancellation() try importJSON(value) } catch { if !Task.isCancelled { self.error = error.localizedDescription } } } case .failure(let error): self.error = error.localizedDescription } } .sheet(isPresented: $pasting, onDismiss: { json = "" }) { VStack(alignment: .leading, spacing: 12) { Text("Paste Service Account JSON").font(.headline) TextEditor(text: $json).font(.body.monospaced()).autocorrectionDisabled() #if os(iOS) .textInputAutocapitalization(.never) #endif .frame(minHeight: 220) if let error { Text(error).foregroundStyle(.red) } HStack { Button("Cancel") { json = "" pasting = false } Spacer() Button("Import") { perform { try importJSON(json) json = "" pasting = false } }.disabled(json.isEmpty) } } .padding().frame(minWidth: 300, idealWidth: 520) } .onDisappear { importTask?.cancel() importTask = nil json = "" } .onChange(of: isEnabled) { _, _ in importTask?.cancel() importTask = nil json = "" pasting = false } } @ViewBuilder private var credentialActions: some View { Button("Import") { importTask?.cancel() error = nil importingFile = true } Button("Paste") { importTask?.cancel() error = nil json = "" pasting = true } if hasCredential { Button("Remove Service Account", role: .destructive) { perform(remove) } } } private func perform(_ action: () throws -> Void) { importTask?.cancel() do { try action() error = nil } catch { self.error = error.localizedDescription } } }